mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-27 14:15:50 +02:00
clean up ui pass 1
This commit is contained in:
parent
3b6a44e683
commit
a0381eb2c6
82 changed files with 17618 additions and 17258 deletions
|
@ -1136,5 +1136,13 @@
|
||||||
"initialSetupTitle": "Initial Server Setup",
|
"initialSetupTitle": "Initial Server Setup",
|
||||||
"initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.",
|
"initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.",
|
||||||
"createAdminAccount": "Create Admin Account",
|
"createAdminAccount": "Create Admin Account",
|
||||||
"setupErrorCreateAdmin": "An error occurred while creating the server admin account."
|
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
||||||
|
"documentation": "Documentation",
|
||||||
|
"saveAllSettings": "Save All Settings",
|
||||||
|
"settingsUpdated": "Settings updated",
|
||||||
|
"settingsUpdatedDescription": "All settings have been updated successfully",
|
||||||
|
"settingsErrorUpdate": "Failed to update settings",
|
||||||
|
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
||||||
|
"sidebarCollapse": "Collapse",
|
||||||
|
"sidebarExpand": "Expand"
|
||||||
}
|
}
|
||||||
|
|
28345
package-lock.json
generated
28345
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -49,6 +49,7 @@
|
||||||
"@radix-ui/react-switch": "1.2.5",
|
"@radix-ui/react-switch": "1.2.5",
|
||||||
"@radix-ui/react-tabs": "1.1.12",
|
"@radix-ui/react-tabs": "1.1.12",
|
||||||
"@radix-ui/react-toast": "1.2.14",
|
"@radix-ui/react-toast": "1.2.14",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@react-email/components": "0.0.41",
|
"@react-email/components": "0.0.41",
|
||||||
"@react-email/render": "^1.1.2",
|
"@react-email/render": "^1.1.2",
|
||||||
"@react-email/tailwind": "1.0.5",
|
"@react-email/tailwind": "1.0.5",
|
||||||
|
|
|
@ -95,7 +95,7 @@ export class Config {
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
|
||||||
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.disable_clients
|
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ import { createNewt, getNewtToken } from "./newt";
|
||||||
import { getOlmToken } from "./olm";
|
import { getOlmToken } from "./olm";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { verifyClientsEnabled } from "@server/middlewares/verifyClientsEnabled";
|
import { verifyClientsEnabled } from "@server/middlewares/verifyClintsEnabled";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { AxiosResponse } from "axios";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { orgLangingNavItems, orgNavItems, rootNavItems } from "../navigation";
|
|
||||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
|
||||||
type OrgPageProps = {
|
type OrgPageProps = {
|
||||||
|
@ -60,7 +59,7 @@ export default async function OrgPage(props: OrgPageProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgId={orgId} navItems={orgLangingNavItems} orgs={orgs}>
|
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
||||||
{overview && (
|
{overview && (
|
||||||
<div className="w-full max-w-4xl mx-auto md:mt-32 mt-4">
|
<div className="w-full max-w-4xl mx-auto md:mt-32 mt-4">
|
||||||
<OrganizationLandingCard
|
<OrganizationLandingCard
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
export type InvitationRow = {
|
export type InvitationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -46,63 +47,69 @@ export default function InvitationsTable({
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
const columns: ColumnDef<InvitationRow>[] = [
|
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",
|
accessorKey: "email",
|
||||||
header: t('email')
|
header: t("email")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "expiresAt",
|
accessorKey: "expiresAt",
|
||||||
header: t('expiresAt'),
|
header: t("expiresAt"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const expiresAt = new Date(row.original.expiresAt);
|
const expiresAt = new Date(row.original.expiresAt);
|
||||||
const isExpired = expiresAt < new Date();
|
const isExpired = expiresAt < new Date();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={isExpired ? "text-red-500" : ""}>
|
<span className={isExpired ? "text-red-500" : ""}>
|
||||||
{expiresAt.toLocaleString()}
|
{moment(expiresAt).format("lll")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "role",
|
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) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('inviteRemoveError'),
|
title: t("inviteRemoveError"),
|
||||||
description: t('inviteRemoveErrorDescription')
|
description: t("inviteRemoveErrorDescription")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: t('inviteRemoved'),
|
title: t("inviteRemoved"),
|
||||||
description: t('inviteRemovedDescription', {email: selectedInvitation.email})
|
description: t("inviteRemovedDescription", {
|
||||||
|
email: selectedInvitation.email
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
setInvitations((prev) =>
|
setInvitations((prev) =>
|
||||||
|
@ -148,20 +157,18 @@ export default function InvitationsTable({
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}
|
{t("inviteQuestionRemove", {
|
||||||
</p>
|
email: selectedInvitation?.email || ""
|
||||||
<p>
|
})}
|
||||||
{t('inviteMessageRemove')}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{t('inviteMessageConfirm')}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>{t("inviteMessageRemove")}</p>
|
||||||
|
<p>{t("inviteMessageConfirm")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('inviteRemoveConfirm')}
|
buttonText={t("inviteRemoveConfirm")}
|
||||||
onConfirm={removeInvitation}
|
onConfirm={removeInvitation}
|
||||||
string={selectedInvitation?.email ?? ""}
|
string={selectedInvitation?.email ?? ""}
|
||||||
title={t('inviteRemove')}
|
title={t("inviteRemove")}
|
||||||
/>
|
/>
|
||||||
<RegenerateInvitationForm
|
<RegenerateInvitationForm
|
||||||
open={isRegenerateModalOpen}
|
open={isRegenerateModalOpen}
|
||||||
|
|
|
@ -201,7 +201,7 @@ export default function RegenerateInvitationForm({
|
||||||
setValidHours(parseInt(value))
|
setValidHours(parseInt(value))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
|
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
@ -159,7 +159,6 @@ export default function DeleteRoleForm({
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
|
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
|
||||||
|
@ -210,13 +209,13 @@ export default function DeleteRoleForm({
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">{t('close')}</Button>
|
<Button variant="outline">{t('close')}</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
|
variant="destructive"
|
||||||
type="submit"
|
type="submit"
|
||||||
form="remove-role-form"
|
form="remove-role-form"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import CreateRoleForm from "./CreateRoleForm";
|
||||||
import DeleteRoleForm from "./DeleteRoleForm";
|
import DeleteRoleForm from "./DeleteRoleForm";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type RoleRow = Role;
|
export type RoleRow = Role;
|
||||||
|
|
||||||
|
@ -42,49 +42,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const columns: ColumnDef<RoleRow>[] = [
|
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",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -95,7 +52,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -103,7 +60,29 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "description",
|
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 OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import RolesTable, { RoleRow } from "./RolesTable";
|
import RolesTable, { RoleRow } from "./RolesTable";
|
||||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
|
||||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type UserRow = {
|
export type UserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -51,65 +51,6 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const columns: ColumnDef<UserRow>[] = [
|
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",
|
accessorKey: "displayUsername",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -120,7 +61,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('username')}
|
{t("username")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -136,7 +77,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('identityProvider')}
|
{t("identityProvider")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -152,7 +93,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('role')}
|
{t("role")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -176,12 +117,68 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
const userRow = row.original;
|
const userRow = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<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 && (
|
{userRow.isOwner && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant={"secondary"}
|
||||||
className="opacity-0 cursor-default"
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
disabled={true}
|
||||||
>
|
>
|
||||||
{t('placeholder')}
|
{t("manage")}
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!userRow.isOwner && (
|
{!userRow.isOwner && (
|
||||||
|
@ -189,10 +186,12 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant={"outlinePrimary"}
|
variant={"secondary"}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
disabled={userRow.isOwner}
|
||||||
>
|
>
|
||||||
{t('manage')}
|
{t("manage")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -210,10 +209,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('userErrorOrgRemove'),
|
title: t("userErrorOrgRemove"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('userErrorOrgRemoveDescription')
|
t("userErrorOrgRemoveDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -221,8 +220,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: t('userOrgRemoved'),
|
title: t("userOrgRemoved"),
|
||||||
description: t('userOrgRemovedDescription', {email: selectedUser.email || ""})
|
description: t("userOrgRemovedDescription", {
|
||||||
|
email: selectedUser.email || ""
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
setUsers((prev) =>
|
setUsers((prev) =>
|
||||||
|
@ -244,19 +245,21 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}
|
{t("userQuestionOrgRemove", {
|
||||||
|
email:
|
||||||
|
selectedUser?.email ||
|
||||||
|
selectedUser?.name ||
|
||||||
|
selectedUser?.username ||
|
||||||
|
""
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>{t("userMessageOrgRemove")}</p>
|
||||||
{t('userMessageOrgRemove')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>{t("userMessageOrgConfirm")}</p>
|
||||||
{t('userMessageOrgConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('userRemoveOrgConfirm')}
|
buttonText={t("userRemoveOrgConfirm")}
|
||||||
onConfirm={removeUser}
|
onConfirm={removeUser}
|
||||||
string={
|
string={
|
||||||
selectedUser?.email ||
|
selectedUser?.email ||
|
||||||
|
@ -264,14 +267,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
selectedUser?.username ||
|
selectedUser?.username ||
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
title={t('userRemoveOrg')}
|
title={t("userRemoveOrg")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UsersDataTable
|
<UsersDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={users}
|
data={users}
|
||||||
inviteUser={() => {
|
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 { GetOrgUserResponse } from "@server/routers/user";
|
||||||
import OrgUserProvider from "@app/providers/OrgUserProvider";
|
import OrgUserProvider from "@app/providers/OrgUserProvider";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
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 { cache } from "react";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
interface UserLayoutProps {
|
interface UserLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -45,7 +37,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
title: t('accessControls'),
|
title: t("accessControls"),
|
||||||
href: "/{orgId}/settings/access/users/{userId}/access-controls"
|
href: "/{orgId}/settings/access/users/{userId}/access-controls"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -54,12 +46,10 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={`${user?.email}`}
|
title={`${user?.email}`}
|
||||||
description={t('userDescription2')}
|
description={t("userDescription2")}
|
||||||
/>
|
/>
|
||||||
<OrgUserProvider orgUser={user}>
|
<OrgUserProvider orgUser={user}>
|
||||||
<HorizontalTabs items={navItems}>
|
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||||
{children}
|
|
||||||
</HorizontalTabs>
|
|
||||||
</OrgUserProvider>
|
</OrgUserProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,40 +78,42 @@ export default function Page() {
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
|
|
||||||
const internalFormSchema = z.object({
|
const internalFormSchema = z.object({
|
||||||
email: z.string().email({ message: t('emailInvalid') }),
|
email: z.string().email({ message: t("emailInvalid") }),
|
||||||
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
|
validForHours: z
|
||||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
|
.string()
|
||||||
|
.min(1, { message: t("inviteValidityDuration") }),
|
||||||
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalFormSchema = z.object({
|
const externalFormSchema = z.object({
|
||||||
username: z.string().min(1, { message: t('usernameRequired') }),
|
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.email({ message: t('emailInvalid') })
|
.email({ message: t("emailInvalid") })
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }),
|
||||||
idpId: z.string().min(1, { message: t('idpSelectPlease') })
|
idpId: z.string().min(1, { message: t("idpSelectPlease") })
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatIdpType = (type: string) => {
|
const formatIdpType = (type: string) => {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case "oidc":
|
case "oidc":
|
||||||
return t('idpGenericOidc');
|
return t("idpGenericOidc");
|
||||||
default:
|
default:
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validFor = [
|
const validFor = [
|
||||||
{ hours: 24, name: t('day', {count: 1}) },
|
{ hours: 24, name: t("day", { count: 1 }) },
|
||||||
{ hours: 48, name: t('day', {count: 2}) },
|
{ hours: 48, name: t("day", { count: 2 }) },
|
||||||
{ hours: 72, name: t('day', {count: 3}) },
|
{ hours: 72, name: t("day", { count: 3 }) },
|
||||||
{ hours: 96, name: t('day', {count: 4}) },
|
{ hours: 96, name: t("day", { count: 4 }) },
|
||||||
{ hours: 120, name: t('day', {count: 5}) },
|
{ hours: 120, name: t("day", { count: 5 }) },
|
||||||
{ hours: 144, name: t('day', {count: 6}) },
|
{ hours: 144, name: t("day", { count: 6 }) },
|
||||||
{ hours: 168, name: t('day', {count: 7}) }
|
{ hours: 168, name: t("day", { count: 7 }) }
|
||||||
];
|
];
|
||||||
|
|
||||||
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
|
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
|
||||||
|
@ -157,10 +159,10 @@ export default function Page() {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('accessRoleErrorFetch'),
|
title: t("accessRoleErrorFetch"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('accessRoleErrorFetchDescription')
|
t("accessRoleErrorFetchDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -180,10 +182,10 @@ export default function Page() {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('idpErrorFetch'),
|
title: t("idpErrorFetch"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('idpErrorFetchDescription')
|
t("idpErrorFetchDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -220,16 +222,16 @@ export default function Page() {
|
||||||
if (e.response?.status === 409) {
|
if (e.response?.status === 409) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('userErrorExists'),
|
title: t("userErrorExists"),
|
||||||
description: t('userErrorExistsDescription')
|
description: t("userErrorExistsDescription")
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('inviteError'),
|
title: t("inviteError"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('inviteErrorDescription')
|
t("inviteErrorDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -239,8 +241,8 @@ export default function Page() {
|
||||||
setInviteLink(res.data.data.inviteLink);
|
setInviteLink(res.data.data.inviteLink);
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: t('userInvited'),
|
title: t("userInvited"),
|
||||||
description: t('userInvitedDescription')
|
description: t("userInvitedDescription")
|
||||||
});
|
});
|
||||||
|
|
||||||
setExpiresInDays(parseInt(values.validForHours) / 24);
|
setExpiresInDays(parseInt(values.validForHours) / 24);
|
||||||
|
@ -266,10 +268,10 @@ export default function Page() {
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('userErrorCreate'),
|
title: t("userErrorCreate"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('userErrorCreateDescription')
|
t("userErrorCreateDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -277,8 +279,8 @@ export default function Page() {
|
||||||
if (res && res.status === 201) {
|
if (res && res.status === 201) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: t('userCreated'),
|
title: t("userCreated"),
|
||||||
description: t('userCreatedDescription')
|
description: t("userCreatedDescription")
|
||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
@ -289,13 +291,13 @@ export default function Page() {
|
||||||
const userTypes: ReadonlyArray<UserTypeOption> = [
|
const userTypes: ReadonlyArray<UserTypeOption> = [
|
||||||
{
|
{
|
||||||
id: "internal",
|
id: "internal",
|
||||||
title: t('userTypeInternal'),
|
title: t("userTypeInternal"),
|
||||||
description: t('userTypeInternalDescription')
|
description: t("userTypeInternalDescription")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "oidc",
|
id: "oidc",
|
||||||
title: t('userTypeExternal'),
|
title: t("userTypeExternal"),
|
||||||
description: t('userTypeExternalDescription')
|
description: t("userTypeExternalDescription")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -303,8 +305,8 @@ export default function Page() {
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
title={t('accessUserCreate')}
|
title={t("accessUserCreate")}
|
||||||
description={t('accessUserCreateDescription')}
|
description={t("accessUserCreateDescription")}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
@ -312,219 +314,239 @@ export default function Page() {
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('userSeeAll')}
|
{t("userSeeAll")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
{!inviteLink && (
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t('userTypeTitle')}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t("userTypeTitle")}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t('userTypeDescription')}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t("userTypeDescription")}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
<SettingsSectionBody>
|
</SettingsSectionHeader>
|
||||||
<StrategySelect
|
<SettingsSectionBody>
|
||||||
options={userTypes}
|
<StrategySelect
|
||||||
defaultValue={userType || undefined}
|
options={userTypes}
|
||||||
onChange={(value) => {
|
defaultValue={userType || undefined}
|
||||||
setUserType(value as UserType);
|
onChange={(value) => {
|
||||||
if (value === "internal") {
|
setUserType(value as UserType);
|
||||||
internalForm.reset();
|
if (value === "internal") {
|
||||||
} else if (value === "oidc") {
|
internalForm.reset();
|
||||||
externalForm.reset();
|
} else if (value === "oidc") {
|
||||||
setSelectedIdp(null);
|
externalForm.reset();
|
||||||
}
|
setSelectedIdp(null);
|
||||||
}}
|
}
|
||||||
cols={2}
|
}}
|
||||||
/>
|
cols={2}
|
||||||
</SettingsSectionBody>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{userType === "internal" && dataLoaded && (
|
{userType === "internal" && dataLoaded && (
|
||||||
<>
|
<>
|
||||||
<SettingsSection>
|
{!inviteLink ? (
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t('userSettings')}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t("userSettings")}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t('userSettingsDescription')}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t("userSettingsDescription")}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
<SettingsSectionBody>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionBody>
|
||||||
<Form {...internalForm}>
|
<SettingsSectionForm>
|
||||||
<form
|
<Form {...internalForm}>
|
||||||
onSubmit={internalForm.handleSubmit(
|
<form
|
||||||
onSubmitInternal
|
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>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
|
id="create-user-form"
|
||||||
{env.email.emailEnabled && (
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<FormField
|
||||||
<Checkbox
|
control={
|
||||||
id="send-email"
|
internalForm.control
|
||||||
checked={sendEmail}
|
}
|
||||||
onCheckedChange={(
|
name="email"
|
||||||
e
|
render={({ field }) => (
|
||||||
) =>
|
<FormItem>
|
||||||
setSendEmail(
|
<FormLabel>
|
||||||
e as boolean
|
{t("email")}
|
||||||
)
|
</FormLabel>
|
||||||
}
|
|
||||||
/>
|
|
||||||
<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
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<Input
|
||||||
<SelectValue placeholder={t('selectDuration')} />
|
{...field}
|
||||||
</SelectTrigger>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
{validFor.map(
|
</FormItem>
|
||||||
(
|
)}
|
||||||
option
|
/>
|
||||||
) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.hours
|
|
||||||
}
|
|
||||||
value={option.hours.toString()}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
option.name
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={
|
control={
|
||||||
internalForm.control
|
internalForm.control
|
||||||
}
|
}
|
||||||
name="roleId"
|
name="validForHours"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('role')}
|
{t(
|
||||||
</FormLabel>
|
"inviteValid"
|
||||||
<Select
|
)}
|
||||||
onValueChange={
|
</FormLabel>
|
||||||
field.onChange
|
<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>
|
{t(
|
||||||
<SelectTrigger>
|
"inviteEmailSent"
|
||||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
)}
|
||||||
</SelectTrigger>
|
</label>
|
||||||
</FormControl>
|
</div>
|
||||||
<SelectContent>
|
|
||||||
{roles.map(
|
|
||||||
(
|
|
||||||
role
|
|
||||||
) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
role.roleId
|
|
||||||
}
|
|
||||||
value={role.roleId.toString()}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
role.name
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</form>
|
||||||
|
</Form>
|
||||||
{inviteLink && (
|
</SettingsSectionForm>
|
||||||
<div className="max-w-md space-y-4">
|
</SettingsSectionBody>
|
||||||
{sendEmail && (
|
</SettingsSection>
|
||||||
<p>
|
) : (
|
||||||
{t('inviteEmailSentDescription')}
|
<SettingsSection>
|
||||||
</p>
|
<SettingsSectionHeader>
|
||||||
)}
|
<SettingsSectionTitle>
|
||||||
{!sendEmail && (
|
{t("userInvited")}
|
||||||
<p>
|
</SettingsSectionTitle>
|
||||||
{t('inviteSentDescription')}
|
<SettingsSectionDescription>
|
||||||
</p>
|
{sendEmail
|
||||||
)}
|
? t("inviteEmailSentDescription")
|
||||||
<p>
|
: t("inviteSentDescription")}
|
||||||
{t('inviteExpiresIn', {days: expiresInDays})}
|
</SettingsSectionDescription>
|
||||||
</p>
|
</SettingsSectionHeader>
|
||||||
<CopyTextBox
|
<SettingsSectionBody>
|
||||||
text={inviteLink}
|
<div className="space-y-4">
|
||||||
wrapText={false}
|
<p>
|
||||||
/>
|
{t("inviteExpiresIn", {
|
||||||
</div>
|
days: expiresInDays
|
||||||
)}
|
})}
|
||||||
</form>
|
</p>
|
||||||
</Form>
|
<CopyTextBox
|
||||||
</SettingsSectionForm>
|
text={inviteLink}
|
||||||
</SettingsSectionBody>
|
wrapText={false}
|
||||||
</SettingsSection>
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -533,16 +555,16 @@ export default function Page() {
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t('idpTitle')}
|
{t("idpTitle")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t('idpSelect')}
|
{t("idpSelect")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{idps.length === 0 ? (
|
{idps.length === 0 ? (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('idpNotConfigured')}
|
{t("idpNotConfigured")}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Form {...externalForm}>
|
<Form {...externalForm}>
|
||||||
|
@ -596,10 +618,10 @@ export default function Page() {
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t('userSettings')}
|
{t("userSettings")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t('userSettingsDescription')}
|
{t("userSettingsDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
|
@ -620,7 +642,9 @@ export default function Page() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('username')}
|
{t(
|
||||||
|
"username"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
@ -628,7 +652,9 @@ export default function Page() {
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{t('usernameUniq')}
|
{t(
|
||||||
|
"usernameUniq"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -643,7 +669,9 @@ export default function Page() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('emailOptional')}
|
{t(
|
||||||
|
"emailOptional"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
@ -663,7 +691,9 @@ export default function Page() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('nameOptional')}
|
{t(
|
||||||
|
"nameOptional"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
@ -683,7 +713,7 @@ export default function Page() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('role')}
|
{t("role")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={
|
onValueChange={
|
||||||
|
@ -691,8 +721,12 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"accessRoleSelect"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
@ -736,19 +770,17 @@ export default function Page() {
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('cancel')}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
{userType && dataLoaded && (
|
{userType && dataLoaded && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type={inviteLink ? "button" : "submit"}
|
||||||
form="create-user-form"
|
form={inviteLink ? undefined : "create-user-form"}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={
|
disabled={loading}
|
||||||
loading ||
|
onClick={inviteLink ? () => router.push(`/${orgId}/settings/access/users`) : undefined}
|
||||||
(userType === "internal" && inviteLink !== null)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t('accessUserCreate')}
|
{inviteLink ? t("done") : t("accessUserCreate")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,11 +50,14 @@ export default function OrgApiKeysTable({
|
||||||
const deleteSite = (apiKeyId: string) => {
|
const deleteSite = (apiKeyId: string) => {
|
||||||
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('apiKeysErrorDelete'), e);
|
console.error(t("apiKeysErrorDelete"), e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('apiKeysErrorDelete'),
|
title: t("apiKeysErrorDelete"),
|
||||||
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("apiKeysErrorDeleteMessage")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -68,41 +71,6 @@ export default function OrgApiKeysTable({
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<OrgApiKeyRow>[] = [
|
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",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -113,7 +81,7 @@ export default function OrgApiKeysTable({
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -121,7 +89,7 @@ export default function OrgApiKeysTable({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "key",
|
accessorKey: "key",
|
||||||
header: t('key'),
|
header: t("key"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return <span className="font-mono">{r.key}</span>;
|
return <span className="font-mono">{r.key}</span>;
|
||||||
|
@ -129,10 +97,10 @@ export default function OrgApiKeysTable({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: t('createdAt'),
|
header: t("createdAt"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
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;
|
const r = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<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}`}>
|
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<Button
|
||||||
{t('edit')}
|
variant={"secondary"}
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -165,24 +167,23 @@ export default function OrgApiKeysTable({
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
|
{t("apiKeysQuestionRemove", {
|
||||||
|
selectedApiKey:
|
||||||
|
selected?.name || selected?.id
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>
|
<b>{t("apiKeysMessageRemove")}</b>
|
||||||
{t('apiKeysMessageRemove')}
|
|
||||||
</b>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>{t("apiKeysMessageConfirm")}</p>
|
||||||
{t('apiKeysMessageConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('apiKeysDeleteConfirm')}
|
buttonText={t("apiKeysDeleteConfirm")}
|
||||||
onConfirm={async () => deleteSite(selected!.id)}
|
onConfirm={async () => deleteSite(selected!.id)}
|
||||||
string={selected.name}
|
string={selected.name}
|
||||||
title={t('apiKeysDelete')}
|
title={t("apiKeysDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
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 SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
|
|
|
@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { Button } from "@app/components/ui/button";
|
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 { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
import {
|
||||||
CreateOrgApiKeyBody,
|
CreateOrgApiKeyBody,
|
||||||
CreateOrgApiKeyResponse
|
CreateOrgApiKeyResponse
|
||||||
|
@ -110,7 +101,7 @@ export default function Page() {
|
||||||
const copiedForm = useForm<CopiedFormValues>({
|
const copiedForm = useForm<CopiedFormValues>({
|
||||||
resolver: zodResolver(copiedFormSchema),
|
resolver: zodResolver(copiedFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
copied: false
|
copied: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -308,54 +299,54 @@ export default function Page() {
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<h4 className="font-semibold">
|
{/* <h4 className="font-semibold"> */}
|
||||||
{t('apiKeysInfo')}
|
{/* {t('apiKeysInfo')} */}
|
||||||
</h4>
|
{/* </h4> */}
|
||||||
|
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form {...copiedForm}>
|
{/* <Form {...copiedForm}> */}
|
||||||
<form
|
{/* <form */}
|
||||||
className="space-y-4"
|
{/* className="space-y-4" */}
|
||||||
id="copied-form"
|
{/* id="copied-form" */}
|
||||||
>
|
{/* > */}
|
||||||
<FormField
|
{/* <FormField */}
|
||||||
control={copiedForm.control}
|
{/* control={copiedForm.control} */}
|
||||||
name="copied"
|
{/* name="copied" */}
|
||||||
render={({ field }) => (
|
{/* render={({ field }) => ( */}
|
||||||
<FormItem>
|
{/* <FormItem> */}
|
||||||
<div className="flex items-center space-x-2">
|
{/* <div className="flex items-center space-x-2"> */}
|
||||||
<Checkbox
|
{/* <Checkbox */}
|
||||||
id="terms"
|
{/* id="terms" */}
|
||||||
defaultChecked={
|
{/* defaultChecked={ */}
|
||||||
copiedForm.getValues(
|
{/* copiedForm.getValues( */}
|
||||||
"copied"
|
{/* "copied" */}
|
||||||
) as boolean
|
{/* ) as boolean */}
|
||||||
}
|
{/* } */}
|
||||||
onCheckedChange={(
|
{/* onCheckedChange={( */}
|
||||||
e
|
{/* e */}
|
||||||
) => {
|
{/* ) => { */}
|
||||||
copiedForm.setValue(
|
{/* copiedForm.setValue( */}
|
||||||
"copied",
|
{/* "copied", */}
|
||||||
e as boolean
|
{/* e as boolean */}
|
||||||
);
|
{/* ); */}
|
||||||
}}
|
{/* }} */}
|
||||||
/>
|
{/* /> */}
|
||||||
<label
|
{/* <label */}
|
||||||
htmlFor="terms"
|
{/* htmlFor="terms" */}
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
|
||||||
>
|
{/* > */}
|
||||||
{t('apiKeysConfirmCopy')}
|
{/* {t('apiKeysConfirmCopy')} */}
|
||||||
</label>
|
{/* </label> */}
|
||||||
</div>
|
{/* </div> */}
|
||||||
<FormMessage />
|
{/* <FormMessage /> */}
|
||||||
</FormItem>
|
{/* </FormItem> */}
|
||||||
)}
|
{/* )} */}
|
||||||
/>
|
{/* /> */}
|
||||||
</form>
|
{/* </form> */}
|
||||||
</Form>
|
{/* </Form> */}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -246,7 +246,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||||
<Link
|
<Link
|
||||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<Button variant={"secondary"} className="ml-2">
|
||||||
Edit
|
Edit
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { GetClientResponse } from "@server/routers/client";
|
import { GetClientResponse } from "@server/routers/client";
|
||||||
import ClientInfoCard from "./ClientInfoCard";
|
import ClientInfoCard from "./ClientInfoCard";
|
||||||
|
|
|
@ -18,9 +18,9 @@ import { GetOrgUserResponse } from "@server/routers/user";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
|
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
|
||||||
import { orgNavItems } from "@app/app/navigation";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { orgNavSections } from "@app/app/navigation";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -82,24 +82,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} 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 (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>
|
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections}>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|
|
@ -31,7 +31,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type ResourceRow = {
|
export type ResourceRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -65,11 +65,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
const deleteResource = (resourceId: number) => {
|
const deleteResource = (resourceId: number) => {
|
||||||
api.delete(`/resource/${resourceId}`)
|
api.delete(`/resource/${resourceId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('resourceErrorDelte'), e);
|
console.error(t("resourceErrorDelte"), e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('resourceErrorDelte'),
|
title: t("resourceErrorDelte"),
|
||||||
description: formatAxiosError(e, t('resourceErrorDelte'))
|
description: formatAxiosError(e, t("resourceErrorDelte"))
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -89,50 +89,16 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('resourcesErrorUpdate'),
|
title: t("resourcesErrorUpdate"),
|
||||||
description: formatAxiosError(e, t('resourcesErrorUpdateDescription'))
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("resourcesErrorUpdateDescription")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<ResourceRow>[] = [
|
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",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -143,7 +109,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -159,7 +125,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('site')}
|
{t("site")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -170,7 +136,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
<Link
|
<Link
|
||||||
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteId}`}
|
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteId}`}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline" size="sm">
|
||||||
{resourceRow.site}
|
{resourceRow.site}
|
||||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -180,7 +146,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "protocol",
|
accessorKey: "protocol",
|
||||||
header: t('protocol'),
|
header: t("protocol"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
||||||
|
@ -188,7 +154,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
header: t('access'),
|
header: t("access"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return (
|
||||||
|
@ -218,7 +184,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('authentication')}
|
{t("authentication")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -230,12 +196,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
{resourceRow.authState === "protected" ? (
|
{resourceRow.authState === "protected" ? (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
<ShieldCheck className="w-4 h-4" />
|
<ShieldCheck className="w-4 h-4" />
|
||||||
<span>{t('protected')}</span>
|
<span>{t("protected")}</span>
|
||||||
</span>
|
</span>
|
||||||
) : resourceRow.authState === "not_protected" ? (
|
) : resourceRow.authState === "not_protected" ? (
|
||||||
<span className="text-yellow-500 flex items-center space-x-2">
|
<span className="text-yellow-500 flex items-center space-x-2">
|
||||||
<ShieldOff className="w-4 h-4" />
|
<ShieldOff className="w-4 h-4" />
|
||||||
<span>{t('notProtected')}</span>
|
<span>{t("notProtected")}</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
|
@ -246,7 +212,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: t('enabled'),
|
header: t("enabled"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.original.enabled}
|
defaultChecked={row.original.enabled}
|
||||||
|
@ -262,11 +228,41 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<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
|
<Link
|
||||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<Button variant={"secondary"} className="ml-2" size="sm">
|
||||||
{t('edit')}
|
{t("edit")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -288,22 +284,22 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{t('resourceQuestionRemove', {selectedResource: selectedResource?.name || selectedResource?.id})}
|
{t("resourceQuestionRemove", {
|
||||||
|
selectedResource:
|
||||||
|
selectedResource?.name ||
|
||||||
|
selectedResource?.id
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">{t("resourceMessageRemove")}</p>
|
||||||
{t('resourceMessageRemove')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>{t("resourceMessageConfirm")}</p>
|
||||||
{t('resourceMessageConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('resourceDeleteConfirm')}
|
buttonText={t("resourceDeleteConfirm")}
|
||||||
onConfirm={async () => deleteResource(selectedResource!.id)}
|
onConfirm={async () => deleteResource(selectedResource!.id)}
|
||||||
string={selectedResource.name}
|
string={selectedResource.name}
|
||||||
title={t('resourceDelete')}
|
title={t("resourceDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -568,7 +568,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outlinePrimary"
|
variant="secondary"
|
||||||
onClick={
|
onClick={
|
||||||
authInfo.password
|
authInfo.password
|
||||||
? removeResourcePassword
|
? removeResourcePassword
|
||||||
|
@ -593,7 +593,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outlinePrimary"
|
variant="secondary"
|
||||||
onClick={
|
onClick={
|
||||||
authInfo.pincode
|
authInfo.pincode
|
||||||
? removeResourcePincode
|
? removeResourcePincode
|
||||||
|
|
|
@ -128,7 +128,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t('proxyErrorInvalidHeader')
|
message: t("proxyErrorInvalidHeader")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t('proxyErrorTls')
|
message: t("proxyErrorTls")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -203,10 +203,10 @@ export default function ReverseProxyTargets(props: {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('targetErrorFetch'),
|
title: t("targetErrorFetch"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
t('targetErrorFetchDescription')
|
t("targetErrorFetchDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -228,10 +228,10 @@ export default function ReverseProxyTargets(props: {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('siteErrorFetch'),
|
title: t("siteErrorFetch"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
t('siteErrorFetchDescription')
|
t("siteErrorFetchDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -251,8 +251,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('targetErrorDuplicate'),
|
title: t("targetErrorDuplicate"),
|
||||||
description: t('targetErrorDuplicateDescription')
|
description: t("targetErrorDuplicateDescription")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -264,8 +264,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
if (!isIPInSubnet(targetIp, subnet)) {
|
if (!isIPInSubnet(targetIp, subnet)) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('targetWireGuardErrorInvalidIp'),
|
title: t("targetWireGuardErrorInvalidIp"),
|
||||||
description: t('targetWireGuardErrorInvalidIpDescription')
|
description: t("targetWireGuardErrorInvalidIpDescription")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -307,10 +307,13 @@ export default function ReverseProxyTargets(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTargets() {
|
async function saveAllSettings() {
|
||||||
try {
|
try {
|
||||||
setTargetsLoading(true);
|
setTargetsLoading(true);
|
||||||
|
setHttpsTlsLoading(true);
|
||||||
|
setProxySettingsLoading(true);
|
||||||
|
|
||||||
|
// Save targets
|
||||||
for (let target of targets) {
|
for (let target of targets) {
|
||||||
const data = {
|
const data = {
|
||||||
ip: target.ip,
|
ip: target.ip,
|
||||||
|
@ -342,9 +345,31 @@ export default function ReverseProxyTargets(props: {
|
||||||
});
|
});
|
||||||
updateResource({ stickySession: stickySessionData.stickySession });
|
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({
|
toast({
|
||||||
title: t('targetsUpdated'),
|
title: t("settingsUpdated"),
|
||||||
description: t('targetsUpdatedDescription')
|
description: t("settingsUpdatedDescription")
|
||||||
});
|
});
|
||||||
|
|
||||||
setTargetsToRemove([]);
|
setTargetsToRemove([]);
|
||||||
|
@ -353,73 +378,15 @@ export default function ReverseProxyTargets(props: {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('targetsErrorUpdate'),
|
title: t("settingsErrorUpdate"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
t('targetsErrorUpdateDescription')
|
t("settingsErrorUpdateDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setTargetsLoading(false);
|
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);
|
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);
|
setProxySettingsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,7 +394,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
const columns: ColumnDef<LocalTarget>[] = [
|
const columns: ColumnDef<LocalTarget>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: t('targetAddr'),
|
header: t("targetAddr"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Input
|
<Input
|
||||||
defaultValue={row.original.ip}
|
defaultValue={row.original.ip}
|
||||||
|
@ -442,7 +409,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "port",
|
accessorKey: "port",
|
||||||
header: t('targetPort'),
|
header: t("targetPort"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -476,7 +443,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: t('enabled'),
|
header: t("enabled"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.original.enabled}
|
defaultChecked={row.original.enabled}
|
||||||
|
@ -503,7 +470,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => removeTarget(row.original.targetId)}
|
onClick={() => removeTarget(row.original.targetId)}
|
||||||
>
|
>
|
||||||
{t('delete')}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -514,7 +481,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
if (resource.http) {
|
if (resource.http) {
|
||||||
const methodCol: ColumnDef<LocalTarget> = {
|
const methodCol: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "method",
|
accessorKey: "method",
|
||||||
header: t('method'),
|
header: t("method"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={row.original.method ?? ""}
|
defaultValue={row.original.method ?? ""}
|
||||||
|
@ -561,11 +528,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>{t("targets")}</SettingsSectionTitle>
|
||||||
{t('targets')}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t('targetsDescription')}
|
{t("targetsDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
|
@ -573,7 +538,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Form {...targetsSettingsForm}>
|
<Form {...targetsSettingsForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={targetsSettingsForm.handleSubmit(
|
onSubmit={targetsSettingsForm.handleSubmit(
|
||||||
saveTargets
|
saveAllSettings
|
||||||
)}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="targets-settings-form"
|
id="targets-settings-form"
|
||||||
|
@ -587,8 +552,12 @@ export default function ReverseProxyTargets(props: {
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SwitchInput
|
<SwitchInput
|
||||||
id="sticky-toggle"
|
id="sticky-toggle"
|
||||||
label={t('targetStickySessions')}
|
label={t(
|
||||||
description={t('targetStickySessionsDescription')}
|
"targetStickySessions"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"targetStickySessionsDescription"
|
||||||
|
)}
|
||||||
defaultChecked={
|
defaultChecked={
|
||||||
field.value
|
field.value
|
||||||
}
|
}
|
||||||
|
@ -619,7 +588,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
name="method"
|
name="method"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('method')}</FormLabel>
|
<FormLabel>
|
||||||
|
{t("method")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={
|
||||||
|
@ -635,8 +606,15 @@ export default function ReverseProxyTargets(props: {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="method">
|
<SelectTrigger
|
||||||
<SelectValue placeholder={t('methodSelect')} />
|
id="method"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"methodSelect"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">
|
<SelectItem value="http">
|
||||||
|
@ -662,7 +640,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
name="ip"
|
name="ip"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative">
|
<FormItem className="relative">
|
||||||
<FormLabel>{t('targetAddr')}</FormLabel>
|
<FormLabel>
|
||||||
|
{t("targetAddr")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input id="ip" {...field} />
|
<Input id="ip" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -695,7 +675,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
name="port"
|
name="port"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('targetPort')}</FormLabel>
|
<FormLabel>
|
||||||
|
{t("targetPort")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
id="port"
|
id="port"
|
||||||
|
@ -710,11 +692,11 @@ export default function ReverseProxyTargets(props: {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outlinePrimary"
|
variant="secondary"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
disabled={!(watchedIp && watchedPort)}
|
disabled={!(watchedIp && watchedPort)}
|
||||||
>
|
>
|
||||||
{t('targetSubmit')}
|
{t("targetSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -758,189 +740,170 @@ export default function ReverseProxyTargets(props: {
|
||||||
colSpan={columns.length}
|
colSpan={columns.length}
|
||||||
className="h-24 text-center"
|
className="h-24 text-center"
|
||||||
>
|
>
|
||||||
{t('targetNoOne')}
|
{t("targetNoOne")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableCaption>
|
{/* <TableCaption> */}
|
||||||
{t('targetNoOneDescription')}
|
{/* {t('targetNoOneDescription')} */}
|
||||||
</TableCaption>
|
{/* </TableCaption> */}
|
||||||
</Table>
|
</Table>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button
|
|
||||||
onClick={saveTargets}
|
|
||||||
loading={targetsLoading}
|
|
||||||
disabled={targetsLoading}
|
|
||||||
form="targets-settings-form"
|
|
||||||
>
|
|
||||||
{t('targetsSubmit')}
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
{resource.http && (
|
{resource.http && (
|
||||||
<SettingsSectionGrid cols={2}>
|
<SettingsSection>
|
||||||
<SettingsSection>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionTitle>
|
||||||
<SettingsSectionTitle>
|
{t("proxyAdditional")}
|
||||||
{t('targetTlsSettings')}
|
</SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
<SettingsSectionDescription>
|
||||||
<SettingsSectionDescription>
|
{t("proxyAdditionalDescription")}
|
||||||
{t('targetTlsSettingsDescription')}
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionHeader>
|
||||||
</SettingsSectionHeader>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionForm>
|
||||||
<SettingsSectionForm>
|
<Form {...tlsSettingsForm}>
|
||||||
<Form {...tlsSettingsForm}>
|
<form
|
||||||
<form
|
onSubmit={tlsSettingsForm.handleSubmit(
|
||||||
onSubmit={tlsSettingsForm.handleSubmit(
|
saveAllSettings
|
||||||
saveTlsSettings
|
)}
|
||||||
|
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
|
<div className="flex items-center justify-between space-x-4">
|
||||||
control={tlsSettingsForm.control}
|
<CollapsibleTrigger asChild>
|
||||||
name="ssl"
|
<Button
|
||||||
render={({ field }) => (
|
variant="text"
|
||||||
<FormItem>
|
size="sm"
|
||||||
<FormControl>
|
className="p-0 flex items-center justify-start gap-2 w-full"
|
||||||
<SwitchInput
|
>
|
||||||
id="ssl-toggle"
|
<p className="text-sm text-muted-foreground">
|
||||||
label={t('proxyEnableSSL')}
|
{t(
|
||||||
defaultChecked={
|
"targetTlsSettingsAdvanced"
|
||||||
field.value
|
)}
|
||||||
}
|
</p>
|
||||||
onCheckedChange={(
|
<div>
|
||||||
val
|
<ChevronsUpDown className="h-4 w-4" />
|
||||||
) => {
|
<span className="sr-only">
|
||||||
field.onChange(
|
Toggle
|
||||||
val
|
</span>
|
||||||
);
|
</div>
|
||||||
}}
|
</Button>
|
||||||
/>
|
</CollapsibleTrigger>
|
||||||
</FormControl>
|
</div>
|
||||||
</FormItem>
|
<CollapsibleContent className="space-y-2">
|
||||||
)}
|
<FormField
|
||||||
/>
|
control={
|
||||||
<Collapsible
|
tlsSettingsForm.control
|
||||||
open={isAdvancedOpen}
|
}
|
||||||
onOpenChange={setIsAdvancedOpen}
|
name="tlsServerName"
|
||||||
className="space-y-2"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
<div className="flex items-center justify-between space-x-4">
|
<FormLabel>
|
||||||
<CollapsibleTrigger asChild>
|
{t("targetTlsSni")}
|
||||||
<Button
|
</FormLabel>
|
||||||
variant="text"
|
<FormControl>
|
||||||
size="sm"
|
<Input {...field} />
|
||||||
className="p-0 flex items-center justify-start gap-2 w-full"
|
</FormControl>
|
||||||
>
|
<FormDescription>
|
||||||
<p className="text-sm text-muted-foreground">
|
{t(
|
||||||
{t('targetTlsSettingsAdvanced')}
|
"targetTlsSniDescription"
|
||||||
</p>
|
)}
|
||||||
<div>
|
</FormDescription>
|
||||||
<ChevronsUpDown className="h-4 w-4" />
|
<FormMessage />
|
||||||
<span className="sr-only">
|
</FormItem>
|
||||||
Toggle
|
)}
|
||||||
</span>
|
/>
|
||||||
</div>
|
</CollapsibleContent>
|
||||||
</Button>
|
</Collapsible>
|
||||||
</CollapsibleTrigger>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
<CollapsibleContent className="space-y-2">
|
</SettingsSectionForm>
|
||||||
<FormField
|
|
||||||
control={
|
<SettingsSectionForm>
|
||||||
tlsSettingsForm.control
|
<Form {...proxySettingsForm}>
|
||||||
}
|
<form
|
||||||
name="tlsServerName"
|
onSubmit={proxySettingsForm.handleSubmit(
|
||||||
render={({ field }) => (
|
saveAllSettings
|
||||||
<FormItem>
|
)}
|
||||||
<FormLabel>
|
className="space-y-4"
|
||||||
{t('targetTlsSni')}
|
id="proxy-settings-form"
|
||||||
</FormLabel>
|
>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Input
|
control={proxySettingsForm.control}
|
||||||
{...field}
|
name="setHostHeader"
|
||||||
/>
|
render={({ field }) => (
|
||||||
</FormControl>
|
<FormItem>
|
||||||
<FormDescription>
|
<FormLabel>
|
||||||
{t('targetTlsSniDescription')}
|
{t("proxyCustomHeader")}
|
||||||
</FormDescription>
|
</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"proxyCustomHeaderDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
</FormDescription>
|
||||||
</CollapsibleContent>
|
<FormMessage />
|
||||||
</Collapsible>
|
</FormItem>
|
||||||
</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
|
|
||||||
)}
|
)}
|
||||||
className="space-y-4"
|
/>
|
||||||
id="proxy-settings-form"
|
</form>
|
||||||
>
|
</Form>
|
||||||
<FormField
|
</SettingsSectionForm>
|
||||||
control={proxySettingsForm.control}
|
</SettingsSectionBody>
|
||||||
name="setHostHeader"
|
</SettingsSection>
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={saveAllSettings}
|
||||||
|
loading={
|
||||||
|
targetsLoading ||
|
||||||
|
httpsTlsLoading ||
|
||||||
|
proxySettingsLoading
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
targetsLoading ||
|
||||||
|
httpsTlsLoading ||
|
||||||
|
proxySettingsLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("saveAllSettings")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -953,7 +916,7 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
if (mask < 0 || mask > 32) {
|
if (mask < 0 || mask > 32) {
|
||||||
throw new Error(t('subnetMaskErrorInvalid'));
|
throw new Error(t("subnetMaskErrorInvalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert IP addresses to binary numbers
|
// Convert IP addresses to binary numbers
|
||||||
|
@ -973,14 +936,14 @@ function ipToNumber(ip: string): number {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
if (parts.length !== 4) {
|
if (parts.length !== 4) {
|
||||||
throw new Error(t('ipAddressErrorInvalidFormat'));
|
throw new Error(t("ipAddressErrorInvalidFormat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert IP octets to 32-bit number
|
// Convert IP octets to 32-bit number
|
||||||
return parts.reduce((num, octet) => {
|
return parts.reduce((num, octet) => {
|
||||||
const oct = parseInt(octet);
|
const oct = parseInt(octet);
|
||||||
if (isNaN(oct) || oct < 0 || oct > 255) {
|
if (isNaN(oct) || oct < 0 || oct > 255) {
|
||||||
throw new Error(t('ipAddressErrorInvalidOctet'));
|
throw new Error(t("ipAddressErrorInvalidOctet"));
|
||||||
}
|
}
|
||||||
return (num << 8) + oct;
|
return (num << 8) + oct;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
|
@ -36,7 +36,6 @@ import {
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow
|
||||||
|
@ -543,48 +542,48 @@ export default function ResourceRules(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<Alert className="hidden md:block">
|
{/* <Alert className="hidden md:block"> */}
|
||||||
<InfoIcon className="h-4 w-4" />
|
{/* <InfoIcon className="h-4 w-4" /> */}
|
||||||
<AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle>
|
{/* <AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle> */}
|
||||||
<AlertDescription className="mt-4">
|
{/* <AlertDescription className="mt-4"> */}
|
||||||
<div className="space-y-1 mb-4">
|
{/* <div className="space-y-1 mb-4"> */}
|
||||||
<p>
|
{/* <p> */}
|
||||||
{t('rulesAboutDescription')}
|
{/* {t('rulesAboutDescription')} */}
|
||||||
</p>
|
{/* </p> */}
|
||||||
</div>
|
{/* </div> */}
|
||||||
<InfoSections cols={2}>
|
{/* <InfoSections cols={2}> */}
|
||||||
<InfoSection>
|
{/* <InfoSection> */}
|
||||||
<InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle>
|
{/* <InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle> */}
|
||||||
<ul className="text-sm text-muted-foreground space-y-1">
|
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
|
||||||
<li className="flex items-center gap-2">
|
{/* <li className="flex items-center gap-2"> */}
|
||||||
<Check className="text-green-500 w-4 h-4" />
|
{/* <Check className="text-green-500 w-4 h-4" /> */}
|
||||||
{t('rulesActionAlwaysAllow')}
|
{/* {t('rulesActionAlwaysAllow')} */}
|
||||||
</li>
|
{/* </li> */}
|
||||||
<li className="flex items-center gap-2">
|
{/* <li className="flex items-center gap-2"> */}
|
||||||
<X className="text-red-500 w-4 h-4" />
|
{/* <X className="text-red-500 w-4 h-4" /> */}
|
||||||
{t('rulesActionAlwaysDeny')}
|
{/* {t('rulesActionAlwaysDeny')} */}
|
||||||
</li>
|
{/* </li> */}
|
||||||
</ul>
|
{/* </ul> */}
|
||||||
</InfoSection>
|
{/* </InfoSection> */}
|
||||||
<InfoSection>
|
{/* <InfoSection> */}
|
||||||
<InfoSectionTitle>
|
{/* <InfoSectionTitle> */}
|
||||||
{t('rulesMatchCriteria')}
|
{/* {t('rulesMatchCriteria')} */}
|
||||||
</InfoSectionTitle>
|
{/* </InfoSectionTitle> */}
|
||||||
<ul className="text-sm text-muted-foreground space-y-1">
|
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
|
||||||
<li className="flex items-center gap-2">
|
{/* <li className="flex items-center gap-2"> */}
|
||||||
{t('rulesMatchCriteriaIpAddress')}
|
{/* {t('rulesMatchCriteriaIpAddress')} */}
|
||||||
</li>
|
{/* </li> */}
|
||||||
<li className="flex items-center gap-2">
|
{/* <li className="flex items-center gap-2"> */}
|
||||||
{t('rulesMatchCriteriaIpAddressRange')}
|
{/* {t('rulesMatchCriteriaIpAddressRange')} */}
|
||||||
</li>
|
{/* </li> */}
|
||||||
<li className="flex items-center gap-2">
|
{/* <li className="flex items-center gap-2"> */}
|
||||||
{t('rulesMatchCriteriaUrl')}
|
{/* {t('rulesMatchCriteriaUrl')} */}
|
||||||
</li>
|
{/* </li> */}
|
||||||
</ul>
|
{/* </ul> */}
|
||||||
</InfoSection>
|
{/* </InfoSection> */}
|
||||||
</InfoSections>
|
{/* </InfoSections> */}
|
||||||
</AlertDescription>
|
{/* </AlertDescription> */}
|
||||||
</Alert>
|
{/* </Alert> */}
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
@ -634,7 +633,7 @@ export default function ResourceRules(props: {
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
@ -664,7 +663,7 @@ export default function ResourceRules(props: {
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
@ -690,7 +689,7 @@ export default function ResourceRules(props: {
|
||||||
control={addRuleForm.control}
|
control={addRuleForm.control}
|
||||||
name="value"
|
name="value"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="space-y-0 mb-2">
|
<FormItem className="gap-1">
|
||||||
<InfoPopup
|
<InfoPopup
|
||||||
text={t('value')}
|
text={t('value')}
|
||||||
info={
|
info={
|
||||||
|
@ -702,7 +701,7 @@ export default function ResourceRules(props: {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -710,8 +709,7 @@ export default function ResourceRules(props: {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outlinePrimary"
|
variant="secondary"
|
||||||
className="mb-2"
|
|
||||||
disabled={!rulesEnabled}
|
disabled={!rulesEnabled}
|
||||||
>
|
>
|
||||||
{t('ruleSubmit')}
|
{t('ruleSubmit')}
|
||||||
|
@ -762,9 +760,9 @@ export default function ResourceRules(props: {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableCaption>
|
{/* <TableCaption> */}
|
||||||
{t('rulesOrder')}
|
{/* {t('rulesOrder')} */}
|
||||||
</TableCaption>
|
{/* </TableCaption> */}
|
||||||
</Table>
|
</Table>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
|
|
|
@ -459,29 +459,31 @@ export default function Page() {
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection>
|
{resourceTypes.length > 1 && (
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t("resourceType")}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t("resourceType")}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t("resourceTypeDescription")}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t("resourceTypeDescription")}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
<SettingsSectionBody>
|
</SettingsSectionHeader>
|
||||||
<StrategySelect
|
<SettingsSectionBody>
|
||||||
options={resourceTypes}
|
<StrategySelect
|
||||||
defaultValue="http"
|
options={resourceTypes}
|
||||||
onChange={(value) => {
|
defaultValue="http"
|
||||||
baseForm.setValue(
|
onChange={(value) => {
|
||||||
"http",
|
baseForm.setValue(
|
||||||
value === "http"
|
"http",
|
||||||
);
|
value === "http"
|
||||||
}}
|
);
|
||||||
cols={2}
|
}}
|
||||||
/>
|
cols={2}
|
||||||
</SettingsSectionBody>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{baseForm.watch("http") ? (
|
{baseForm.watch("http") ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
|
|
|
@ -392,7 +392,7 @@ export default function CreateShareLinkForm({
|
||||||
defaultValue={field.value.toString()}
|
defaultValue={field.value.toString()}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder={t('selectDuration')} />
|
<SelectValue placeholder={t('selectDuration')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
@ -69,11 +69,8 @@ export default function ShareLinksTable({
|
||||||
async function deleteSharelink(id: string) {
|
async function deleteSharelink(id: string) {
|
||||||
await api.delete(`/access-token/${id}`).catch((e) => {
|
await api.delete(`/access-token/${id}`).catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
title: t('shareErrorDelete'),
|
title: t("shareErrorDelete"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
|
||||||
e,
|
|
||||||
t('shareErrorDeleteMessage')
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,53 +78,12 @@ export default function ShareLinksTable({
|
||||||
setRows(newRows);
|
setRows(newRows);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('shareDeleted'),
|
title: t("shareDeleted"),
|
||||||
description: t('shareDeletedDescription')
|
description: t("shareDeletedDescription")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<ShareLinkRow>[] = [
|
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",
|
accessorKey: "resourceName",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -138,7 +94,7 @@ export default function ShareLinksTable({
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('resource')}
|
{t("resource")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -147,7 +103,7 @@ export default function ShareLinksTable({
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return (
|
return (
|
||||||
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
|
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
|
||||||
<Button variant="outline">
|
<Button variant="outline" size="sm">
|
||||||
{r.resourceName}{" "}
|
{r.resourceName}{" "}
|
||||||
{r.siteName ? `(${r.siteName})` : ""}
|
{r.siteName ? `(${r.siteName})` : ""}
|
||||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||||
|
@ -166,7 +122,7 @@ export default function ShareLinksTable({
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('title')}
|
{t("title")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -245,7 +201,7 @@ export default function ShareLinksTable({
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('created')}
|
{t("created")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -265,7 +221,7 @@ export default function ShareLinksTable({
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('expires')}
|
{t("expires")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -275,23 +231,50 @@ export default function ShareLinksTable({
|
||||||
if (r.expiresAt) {
|
if (r.expiresAt) {
|
||||||
return moment(r.expiresAt).format("lll");
|
return moment(r.expiresAt).format("lll");
|
||||||
}
|
}
|
||||||
return t('never');
|
return t("never");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "delete",
|
id: "delete",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<div className="flex items-center justify-end space-x-2">
|
const resourceRow = row.original;
|
||||||
<Button
|
return (
|
||||||
variant="outlinePrimary"
|
<div className="flex items-center justify-end space-x-2">
|
||||||
onClick={() =>
|
{/* <DropdownMenu> */}
|
||||||
deleteSharelink(row.original.accessTokenId)
|
{/* <DropdownMenuTrigger asChild> */}
|
||||||
}
|
{/* <Button variant="ghost" className="h-8 w-8 p-0"> */}
|
||||||
>
|
{/* <span className="sr-only"> */}
|
||||||
{t('delete')}
|
{/* {t("openMenu")} */}
|
||||||
</Button>
|
{/* </span> */}
|
||||||
</div>
|
{/* <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 { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import CreateSiteFormModal from "./CreateSiteModal";
|
import CreateSiteFormModal from "./CreateSiteModal";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { parseDataSize } from '@app/lib/dataSize';
|
import { parseDataSize } from "@app/lib/dataSize";
|
||||||
|
|
||||||
export type SiteRow = {
|
export type SiteRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -81,11 +81,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
const deleteSite = (siteId: number) => {
|
const deleteSite = (siteId: number) => {
|
||||||
api.delete(`/site/${siteId}`)
|
api.delete(`/site/${siteId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('siteErrorDelete'), e);
|
console.error(t("siteErrorDelete"), e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('siteErrorDelete'),
|
title: t("siteErrorDelete"),
|
||||||
description: formatAxiosError(e, t('siteErrorDelete'))
|
description: formatAxiosError(e, t("siteErrorDelete"))
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -99,42 +99,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<SiteRow>[] = [
|
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",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -145,7 +109,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -161,7 +125,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('online')}
|
{t("online")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -176,14 +140,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t('online')}</span>
|
<span>{t("online")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||||
<span>{t('offline')}</span>
|
<span>{t("offline")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -203,7 +167,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
}
|
}
|
||||||
className="hidden md:flex whitespace-nowrap"
|
className="hidden md:flex whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{t('site')}
|
{t("site")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -226,13 +190,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('dataIn')}
|
{t("dataIn")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sortingFn: (rowA, rowB) =>
|
sortingFn: (rowA, rowB) =>
|
||||||
parseDataSize(rowA.original.mbIn) - parseDataSize(rowB.original.mbIn)
|
parseDataSize(rowA.original.mbIn) -
|
||||||
|
parseDataSize(rowB.original.mbIn)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "mbOut",
|
accessorKey: "mbOut",
|
||||||
|
@ -244,13 +209,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('dataOut')}
|
{t("dataOut")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sortingFn: (rowA, rowB) =>
|
sortingFn: (rowA, rowB) =>
|
||||||
parseDataSize(rowA.original.mbOut) - parseDataSize(rowB.original.mbOut),
|
parseDataSize(rowA.original.mbOut) -
|
||||||
|
parseDataSize(rowB.original.mbOut)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "type",
|
accessorKey: "type",
|
||||||
|
@ -262,7 +228,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('connectionType')}
|
{t("connectionType")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -289,7 +255,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
if (originalRow.type === "local") {
|
if (originalRow.type === "local") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span>{t('local')}</span>
|
<span>{t("local")}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -316,12 +282,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const siteRow = row.original;
|
const siteRow = row.original;
|
||||||
return (
|
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
|
<Link
|
||||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<Button variant={"secondary"} size="sm">
|
||||||
{t('edit')}
|
{t("edit")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -343,22 +338,21 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('siteQuestionRemove', {selectedSite: selectedSite?.name || selectedSite?.id})}
|
{t("siteQuestionRemove", {
|
||||||
|
selectedSite:
|
||||||
|
selectedSite?.name || selectedSite?.id
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>{t("siteMessageRemove")}</p>
|
||||||
{t('siteMessageRemove')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>{t("siteMessageConfirm")}</p>
|
||||||
{t('siteMessageConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('siteConfirmDelete')}
|
buttonText={t("siteConfirmDelete")}
|
||||||
onConfirm={async () => deleteSite(selectedSite!.id)}
|
onConfirm={async () => deleteSite(selectedSite!.id)}
|
||||||
string={selectedSite.name}
|
string={selectedSite.name}
|
||||||
title={t('siteDelete')}
|
title={t("siteDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,7 @@ import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowLeft } from "lucide-react";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import SiteInfoCard from "./SiteInfoCard";
|
import SiteInfoCard from "./SiteInfoCard";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
|
|
@ -55,14 +55,6 @@ import {
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
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 { QRCodeCanvas } from "qrcode.react";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
@ -117,7 +109,8 @@ export default function Page() {
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.method !== "local") {
|
if (data.method !== "local") {
|
||||||
return data.copied;
|
// return data.copied;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
@ -622,26 +615,28 @@ WantedBy=default.target`
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection>
|
{tunnelTypes.length > 1 && (
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t('tunnelType')}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t('tunnelType')}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t('siteTunnelDescription')}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t('siteTunnelDescription')}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
<SettingsSectionBody>
|
</SettingsSectionHeader>
|
||||||
<StrategySelect
|
<SettingsSectionBody>
|
||||||
options={tunnelTypes}
|
<StrategySelect
|
||||||
defaultValue={form.getValues("method")}
|
options={tunnelTypes}
|
||||||
onChange={(value) => {
|
defaultValue={form.getValues("method")}
|
||||||
form.setValue("method", value);
|
onChange={(value) => {
|
||||||
}}
|
form.setValue("method", value);
|
||||||
cols={3}
|
}}
|
||||||
/>
|
cols={3}
|
||||||
</SettingsSectionBody>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{form.watch("method") === "newt" && (
|
{form.watch("method") === "newt" && (
|
||||||
<>
|
<>
|
||||||
|
@ -700,46 +695,46 @@ WantedBy=default.target`
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Form {...form}>
|
{/* <Form {...form}> */}
|
||||||
<form
|
{/* <form */}
|
||||||
className="space-y-4"
|
{/* className="space-y-4" */}
|
||||||
id="create-site-form"
|
{/* id="create-site-form" */}
|
||||||
>
|
{/* > */}
|
||||||
<FormField
|
{/* <FormField */}
|
||||||
control={form.control}
|
{/* control={form.control} */}
|
||||||
name="copied"
|
{/* name="copied" */}
|
||||||
render={({ field }) => (
|
{/* render={({ field }) => ( */}
|
||||||
<FormItem>
|
{/* <FormItem> */}
|
||||||
<div className="flex items-center space-x-2">
|
{/* <div className="flex items-center space-x-2"> */}
|
||||||
<Checkbox
|
{/* <Checkbox */}
|
||||||
id="terms"
|
{/* id="terms" */}
|
||||||
defaultChecked={
|
{/* defaultChecked={ */}
|
||||||
form.getValues(
|
{/* form.getValues( */}
|
||||||
"copied"
|
{/* "copied" */}
|
||||||
) as boolean
|
{/* ) as boolean */}
|
||||||
}
|
{/* } */}
|
||||||
onCheckedChange={(
|
{/* onCheckedChange={( */}
|
||||||
e
|
{/* e */}
|
||||||
) => {
|
{/* ) => { */}
|
||||||
form.setValue(
|
{/* form.setValue( */}
|
||||||
"copied",
|
{/* "copied", */}
|
||||||
e as boolean
|
{/* e as boolean */}
|
||||||
);
|
{/* ); */}
|
||||||
}}
|
{/* }} */}
|
||||||
/>
|
{/* /> */}
|
||||||
<label
|
{/* <label */}
|
||||||
htmlFor="terms"
|
{/* htmlFor="terms" */}
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
|
||||||
>
|
{/* > */}
|
||||||
{t('siteConfirmCopy')}
|
{/* {t('siteConfirmCopy')} */}
|
||||||
</label>
|
{/* </label> */}
|
||||||
</div>
|
{/* </div> */}
|
||||||
<FormMessage />
|
{/* <FormMessage /> */}
|
||||||
</FormItem>
|
{/* </FormItem> */}
|
||||||
)}
|
{/* )} */}
|
||||||
/>
|
{/* /> */}
|
||||||
</form>
|
{/* </form> */}
|
||||||
</Form>
|
{/* </Form> */}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
|
|
|
@ -46,11 +46,14 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
const deleteSite = (apiKeyId: string) => {
|
const deleteSite = (apiKeyId: string) => {
|
||||||
api.delete(`/api-key/${apiKeyId}`)
|
api.delete(`/api-key/${apiKeyId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('apiKeysErrorDelete'), e);
|
console.error(t("apiKeysErrorDelete"), e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('apiKeysErrorDelete'),
|
title: t("apiKeysErrorDelete"),
|
||||||
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("apiKeysErrorDeleteMessage")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -64,41 +67,6 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<ApiKeyRow>[] = [
|
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",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -109,7 +77,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -117,7 +85,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "key",
|
accessorKey: "key",
|
||||||
header: t('key'),
|
header: t("key"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return <span className="font-mono">{r.key}</span>;
|
return <span className="font-mono">{r.key}</span>;
|
||||||
|
@ -125,7 +93,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: t('createdAt'),
|
header: t("createdAt"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||||
|
@ -136,13 +104,44 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Link href={`/admin/api-keys/${r.id}`}>
|
<DropdownMenu>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<DropdownMenuTrigger asChild>
|
||||||
{t('edit')}
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<span className="sr-only">
|
||||||
</Button>
|
{t("openMenu")}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -161,24 +160,23 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
|
{t("apiKeysQuestionRemove", {
|
||||||
|
selectedApiKey:
|
||||||
|
selected?.name || selected?.id
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>
|
<b>{t("apiKeysMessageRemove")}</b>
|
||||||
{t('apiKeysMessageRemove')}
|
|
||||||
</b>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>{t("apiKeysMessageConfirm")}</p>
|
||||||
{t('apiKeysMessageConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('apiKeysDeleteConfirm')}
|
buttonText={t("apiKeysDeleteConfirm")}
|
||||||
onConfirm={async () => deleteSite(selected!.id)}
|
onConfirm={async () => deleteSite(selected!.id)}
|
||||||
string={selected.name}
|
string={selected.name}
|
||||||
title={t('apiKeysDelete')}
|
title={t("apiKeysDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
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 SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
|
|
|
@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { Button } from "@app/components/ui/button";
|
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 { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
import {
|
||||||
CreateOrgApiKeyBody,
|
CreateOrgApiKeyBody,
|
||||||
CreateOrgApiKeyResponse
|
CreateOrgApiKeyResponse
|
||||||
|
@ -108,7 +99,7 @@ export default function Page() {
|
||||||
const copiedForm = useForm<CopiedFormValues>({
|
const copiedForm = useForm<CopiedFormValues>({
|
||||||
resolver: zodResolver(copiedFormSchema),
|
resolver: zodResolver(copiedFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
copied: false
|
copied: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -299,54 +290,54 @@ export default function Page() {
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<h4 className="font-semibold">
|
{/* <h4 className="font-semibold"> */}
|
||||||
{t('apiKeysInfo')}
|
{/* {t('apiKeysInfo')} */}
|
||||||
</h4>
|
{/* </h4> */}
|
||||||
|
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form {...copiedForm}>
|
{/* <Form {...copiedForm}> */}
|
||||||
<form
|
{/* <form */}
|
||||||
className="space-y-4"
|
{/* className="space-y-4" */}
|
||||||
id="copied-form"
|
{/* id="copied-form" */}
|
||||||
>
|
{/* > */}
|
||||||
<FormField
|
{/* <FormField */}
|
||||||
control={copiedForm.control}
|
{/* control={copiedForm.control} */}
|
||||||
name="copied"
|
{/* name="copied" */}
|
||||||
render={({ field }) => (
|
{/* render={({ field }) => ( */}
|
||||||
<FormItem>
|
{/* <FormItem> */}
|
||||||
<div className="flex items-center space-x-2">
|
{/* <div className="flex items-center space-x-2"> */}
|
||||||
<Checkbox
|
{/* <Checkbox */}
|
||||||
id="terms"
|
{/* id="terms" */}
|
||||||
defaultChecked={
|
{/* defaultChecked={ */}
|
||||||
copiedForm.getValues(
|
{/* copiedForm.getValues( */}
|
||||||
"copied"
|
{/* "copied" */}
|
||||||
) as boolean
|
{/* ) as boolean */}
|
||||||
}
|
{/* } */}
|
||||||
onCheckedChange={(
|
{/* onCheckedChange={( */}
|
||||||
e
|
{/* e */}
|
||||||
) => {
|
{/* ) => { */}
|
||||||
copiedForm.setValue(
|
{/* copiedForm.setValue( */}
|
||||||
"copied",
|
{/* "copied", */}
|
||||||
e as boolean
|
{/* e as boolean */}
|
||||||
);
|
{/* ); */}
|
||||||
}}
|
{/* }} */}
|
||||||
/>
|
{/* /> */}
|
||||||
<label
|
{/* <label */}
|
||||||
htmlFor="terms"
|
{/* htmlFor="terms" */}
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
|
||||||
>
|
{/* > */}
|
||||||
{t('apiKeysConfirmCopy')}
|
{/* {t('apiKeysConfirmCopy')} */}
|
||||||
</label>
|
{/* </label> */}
|
||||||
</div>
|
{/* </div> */}
|
||||||
<FormMessage />
|
{/* <FormMessage /> */}
|
||||||
</FormItem>
|
{/* </FormItem> */}
|
||||||
)}
|
{/* )} */}
|
||||||
/>
|
{/* /> */}
|
||||||
</form>
|
{/* </form> */}
|
||||||
</Form>
|
{/* </Form> */}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -43,14 +43,14 @@ export default function IdpTable({ idps }: Props) {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/idp/${idpId}`);
|
await api.delete(`/idp/${idpId}`);
|
||||||
toast({
|
toast({
|
||||||
title: t('success'),
|
title: t("success"),
|
||||||
description: t('idpDeletedDescription')
|
description: t("idpDeletedDescription")
|
||||||
});
|
});
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t('error'),
|
title: t("error"),
|
||||||
description: formatAxiosError(e),
|
description: formatAxiosError(e),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
|
@ -67,41 +67,6 @@ export default function IdpTable({ idps }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<IdpRow>[] = [
|
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",
|
accessorKey: "idpId",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -128,7 +93,7 @@ export default function IdpTable({ idps }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -144,7 +109,7 @@ export default function IdpTable({ idps }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('type')}
|
{t("type")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -162,9 +127,43 @@ export default function IdpTable({ idps }: Props) {
|
||||||
const siteRow = row.original;
|
const siteRow = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<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`}>
|
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
<Button
|
||||||
{t('edit')}
|
variant={"secondary"}
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -186,22 +185,20 @@ export default function IdpTable({ idps }: Props) {
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('idpQuestionRemove', {name: selectedIdp.name})}
|
{t("idpQuestionRemove", {
|
||||||
|
name: selectedIdp.name
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<b>
|
<b>{t("idpMessageRemove")}</b>
|
||||||
{t('idpMessageRemove')}
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{t('idpMessageConfirm')}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>{t("idpMessageConfirm")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('idpConfirmDelete')}
|
buttonText={t("idpConfirmDelete")}
|
||||||
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
|
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
|
||||||
string={selectedIdp.name}
|
string={selectedIdp.name}
|
||||||
title={t('idpDelete')}
|
title={t("idpDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,7 @@ import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
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 SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
interface SettingsLayoutProps {
|
||||||
|
|
|
@ -140,7 +140,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant={"outlinePrimary"}
|
variant={"secondary"}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={() => onEdit(policy)}
|
onClick={() => onEdit(policy)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { adminNavItems } from "../navigation";
|
import { adminNavSections } from "../navigation";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export default async function AdminLayout(props: LayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgs={orgs} navItems={adminNavItems}>
|
<Layout orgs={orgs} navItems={adminNavSections}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|
|
@ -122,7 +122,7 @@ export function LicenseKeysDataTable({
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outlinePrimary"
|
variant="secondary"
|
||||||
onClick={() => onDelete(row.original)}
|
onClick={() => onDelete(row.original)}
|
||||||
>
|
>
|
||||||
{t('delete')}
|
{t('delete')}
|
||||||
|
|
|
@ -41,11 +41,11 @@ export default function UsersTable({ users }: Props) {
|
||||||
const deleteUser = (id: string) => {
|
const deleteUser = (id: string) => {
|
||||||
api.delete(`/user/${id}`)
|
api.delete(`/user/${id}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('userErrorDelete'), e);
|
console.error(t("userErrorDelete"), e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('userErrorDelete'),
|
title: t("userErrorDelete"),
|
||||||
description: formatAxiosError(e, t('userErrorDelete'))
|
description: formatAxiosError(e, t("userErrorDelete"))
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -84,7 +84,7 @@ export default function UsersTable({ users }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('username')}
|
{t("username")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -100,7 +100,7 @@ export default function UsersTable({ users }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('email')}
|
{t("email")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -116,7 +116,7 @@ export default function UsersTable({ users }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('name')}
|
{t("name")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -132,7 +132,7 @@ export default function UsersTable({ users }: Props) {
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('identityProvider')}
|
{t("identityProvider")}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -146,14 +146,15 @@ export default function UsersTable({ users }: Props) {
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant={"outlinePrimary"}
|
variant={"secondary"}
|
||||||
|
size="sm"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelected(r);
|
setSelected(r);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('delete')}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -174,26 +175,27 @@ export default function UsersTable({ users }: Props) {
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{t('userQuestionRemove', {selectedUser: selected?.email || selected?.name || selected?.username})}
|
{t("userQuestionRemove", {
|
||||||
|
selectedUser:
|
||||||
|
selected?.email ||
|
||||||
|
selected?.name ||
|
||||||
|
selected?.username
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>
|
<b>{t("userMessageRemove")}</b>
|
||||||
{t('userMessageRemove')}
|
|
||||||
</b>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>{t("userMessageConfirm")}</p>
|
||||||
{t('userMessageConfirm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t('userDeleteConfirm')}
|
buttonText={t("userDeleteConfirm")}
|
||||||
onConfirm={async () => deleteUser(selected!.id)}
|
onConfirm={async () => deleteUser(selected!.id)}
|
||||||
string={
|
string={
|
||||||
selected.email || selected.name || selected.username
|
selected.email || selected.name || selected.username
|
||||||
}
|
}
|
||||||
title={t('userDeleteServer')}
|
title={t("userDeleteServer")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -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 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 "tw-animate-css";
|
||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: hsl(0 0% 98%);
|
--radius: 0.65rem;
|
||||||
--foreground: hsl(20 0% 10%);
|
--background: oklch(0.99 0 0);
|
||||||
--card: hsl(0 0% 100%);
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
--card-foreground: hsl(20 0% 10%);
|
--card: oklch(1 0 0);
|
||||||
--popover: hsl(0 0% 100%);
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
--popover-foreground: hsl(20 0% 10%);
|
--popover: oklch(1 0 0);
|
||||||
--primary: hsl(24.6 95% 53.1%);
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
--primary-foreground: hsl(60 9.1% 97.8%);
|
--primary: oklch(0.6717 0.1946 41.93);
|
||||||
--secondary: hsl(60 4.8% 95.9%);
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--secondary-foreground: hsl(24 9.8% 10%);
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
--muted: hsl(60 4.8% 85%);
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--muted-foreground: hsl(25 5.3% 44.7%);
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
--accent: hsl(60 4.8% 90%);
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
--accent-foreground: hsl(24 9.8% 10%);
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
--destructive: hsl(0 84.2% 60.2%);
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--destructive-foreground: hsl(60 9.1% 97.8%);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: hsl(20 5.9% 90%);
|
--border: oklch(0.92 0.004 286.32);
|
||||||
--input: hsl(20 5.9% 75%);
|
--input: oklch(0.92 0.004 286.32);
|
||||||
--ring: hsl(20 5.9% 75%);
|
--ring: oklch(0.705 0.213 47.604);
|
||||||
--radius: 0.75rem;
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-1: hsl(12 76% 61%);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-2: hsl(173 58% 39%);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--chart-3: hsl(197 37% 24%);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-4: hsl(43 74% 66%);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--chart-5: hsl(27 87% 67%);
|
--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 {
|
.dark {
|
||||||
--background: hsl(20 0% 8%);
|
--background: oklch(0.20 0.006 285.885);
|
||||||
--foreground: hsl(60 9.1% 97.8%);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: hsl(20 0% 10%);
|
--card: oklch(0.21 0.006 285.885);
|
||||||
--card-foreground: hsl(60 9.1% 97.8%);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: hsl(20 0% 10%);
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
--popover-foreground: hsl(60 9.1% 97.8%);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: hsl(20.5 90.2% 48.2%);
|
--primary: oklch(0.6717 0.1946 41.93);
|
||||||
--primary-foreground: hsl(60 9.1% 97.8%);
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--secondary: hsl(12 6.5% 15%);
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
--secondary-foreground: hsl(60 9.1% 97.8%);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: hsl(12 6.5% 25%);
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
--muted-foreground: hsl(24 5.4% 63.9%);
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
--accent: hsl(12 2.5% 15%);
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
--accent-foreground: hsl(60 9.1% 97.8%);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: hsl(0 72.2% 50.6%);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--destructive-foreground: hsl(60 9.1% 97.8%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--border: hsl(12 6.5% 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--input: hsl(12 6.5% 35%);
|
--ring: oklch(0.646 0.222 41.116);
|
||||||
--ring: hsl(12 6.5% 35%);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-1: hsl(220 70% 50%);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-2: hsl(160 60% 45%);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-3: hsl(30 80% 55%);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-4: hsl(280 65% 60%);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--chart-5: hsl(340 75% 55%);
|
--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 {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
|
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
::backdrop,
|
::backdrop,
|
||||||
::file-selector-button {
|
::file-selector-button {
|
||||||
border-color: var(--color-gray-200, currentcolor);
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { AxiosResponse } from "axios";
|
||||||
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
|
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
|
||||||
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
|
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
|
||||||
import { GetLicenseStatusResponse } from "@server/routers/license";
|
import { GetLicenseStatusResponse } from "@server/routers/license";
|
||||||
import LicenseViolation from "./components/LicenseViolation";
|
import LicenseViolation from "@app/components/LicenseViolation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
|
|
|
@ -9,94 +9,97 @@ import {
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
Workflow,
|
Workflow,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
TicketCheck
|
TicketCheck,
|
||||||
|
User
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const orgLangingNavItems: SidebarNavItem[] = [
|
export type SidebarNavSection = {
|
||||||
{
|
heading: string;
|
||||||
title: "sidebarOverview",
|
items: SidebarNavItem[];
|
||||||
href: "/{orgId}",
|
};
|
||||||
icon: <Home className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const rootNavItems: SidebarNavItem[] = [
|
export const orgNavSections: SidebarNavSection[] = [
|
||||||
{
|
{
|
||||||
title: "sidebarHome",
|
heading: "General",
|
||||||
href: "/",
|
items: [
|
||||||
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: [
|
|
||||||
{
|
{
|
||||||
title: "sidebarUsers",
|
title: "sidebarSites",
|
||||||
href: "/{orgId}/settings/access/users",
|
href: "/{orgId}/settings/sites",
|
||||||
children: [
|
icon: <Combine className="h-4 w-4" />
|
||||||
{
|
|
||||||
title: "sidebarInvitations",
|
|
||||||
href: "/{orgId}/settings/access/invitations"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "sidebarRoles",
|
title: "sidebarResources",
|
||||||
href: "/{orgId}/settings/access/roles"
|
href: "/{orgId}/settings/resources",
|
||||||
|
icon: <Waypoints className="h-4 w-4" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "sidebarShareableLinks",
|
heading: "Access Control",
|
||||||
href: "/{orgId}/settings/share-links",
|
items: [
|
||||||
icon: <LinkIcon className="h-4 w-4" />
|
{
|
||||||
|
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",
|
heading: "Organization",
|
||||||
href: "/{orgId}/settings/api-keys",
|
items: [
|
||||||
icon: <KeyRound className="h-4 w-4" />
|
{
|
||||||
},
|
title: "sidebarApiKeys",
|
||||||
{
|
href: "/{orgId}/settings/api-keys",
|
||||||
title: "sidebarSettings",
|
icon: <KeyRound className="h-4 w-4" />
|
||||||
href: "/{orgId}/settings/general",
|
},
|
||||||
icon: <Settings 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",
|
heading: "Admin",
|
||||||
href: "/admin/users",
|
items: [
|
||||||
icon: <Users className="h-4 w-4" />
|
{
|
||||||
},
|
title: "sidebarAllUsers",
|
||||||
{
|
href: "/admin/users",
|
||||||
title: "sidebarApiKeys",
|
icon: <Users className="h-4 w-4" />
|
||||||
href: "/admin/api-keys",
|
},
|
||||||
icon: <KeyRound className="h-4 w-4" />
|
{
|
||||||
},
|
title: "sidebarApiKeys",
|
||||||
{
|
href: "/admin/api-keys",
|
||||||
title: "sidebarIdentityProviders",
|
icon: <KeyRound className="h-4 w-4" />
|
||||||
href: "/admin/idp",
|
},
|
||||||
icon: <Fingerprint className="h-4 w-4" />
|
{
|
||||||
},
|
title: "sidebarIdentityProviders",
|
||||||
{
|
href: "/admin/idp",
|
||||||
title: "sidebarLicense",
|
icon: <Fingerprint className="h-4 w-4" />
|
||||||
href: "/admin/license",
|
},
|
||||||
icon: <TicketCheck 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 { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrganizationLanding from "./components/OrganizationLanding";
|
import OrganizationLanding from "@app/components/OrganizationLanding";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { rootNavItems } from "./navigation";
|
|
||||||
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
||||||
import { cookies } from "next/headers";
|
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 allCookies = await cookies();
|
||||||
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
|
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
|
||||||
|
|
||||||
if (lastOrgCookie && orgs.length > 0) {
|
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) {
|
if (lastOrgExists) {
|
||||||
redirect(`/${lastOrgCookie}`);
|
redirect(`/${lastOrgCookie}`);
|
||||||
} else {
|
} else {
|
||||||
const ownedOrg = orgs.find(org => org.isOwner);
|
const ownedOrg = orgs.find((org) => org.isOwner);
|
||||||
if (ownedOrg) {
|
if (ownedOrg) {
|
||||||
redirect(`/${ownedOrg.orgId}`);
|
redirect(`/${ownedOrg.orgId}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,7 +91,7 @@ export default async function Page(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<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">
|
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
|
||||||
<OrganizationLanding
|
<OrganizationLanding
|
||||||
disableCreateOrg={
|
disableCreateOrg={
|
||||||
|
|
|
@ -6,7 +6,6 @@ import UserProvider from "@app/providers/UserProvider";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import { rootNavItems } from "../navigation";
|
|
||||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
@ -54,11 +53,7 @@ export default async function SetupLayout({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout
|
<Layout navItems={[]} orgs={orgs}>
|
||||||
navItems={rootNavItems}
|
|
||||||
showBreadcrumbs={false}
|
|
||||||
orgs={orgs}
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
|
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
export function AuthFooter() {}
|
|
|
@ -1,42 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Breadcrumbs() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const segments = pathname.split("/").filter(Boolean);
|
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItem[] = segments.map((segment, index) => {
|
|
||||||
const href = `/${segments.slice(0, index + 1).join("/")}`;
|
|
||||||
let label = decodeURIComponent(segment);
|
|
||||||
return { label, href };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="flex items-center space-x-1 text-muted-foreground">
|
|
||||||
{breadcrumbs.map((crumb, index) => (
|
|
||||||
<div key={crumb.href} className="flex items-center flex-nowrap">
|
|
||||||
{index !== 0 && <ChevronRight className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
<Link
|
|
||||||
href={crumb.href}
|
|
||||||
className={cn(
|
|
||||||
"ml-1 hover:text-foreground whitespace-nowrap",
|
|
||||||
index === breadcrumbs.length - 1 &&
|
|
||||||
"text-foreground font-medium"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{crumb.label}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -72,7 +72,7 @@ export default function InviteUserForm({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
string: z.string().refine((val) => val === string, {
|
string: z.string().refine((val) => val === string, {
|
||||||
message: t('inviteErrorInvalidConfirmation')
|
message: t("inviteErrorInvalidConfirmation")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,7 +108,9 @@ export default function InviteUserForm({
|
||||||
<CredenzaTitle>{title}</CredenzaTitle>
|
<CredenzaTitle>{title}</CredenzaTitle>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="mb-4 break-all overflow-hidden">{dialog}</div>
|
<div className="mb-4 break-all overflow-hidden">
|
||||||
|
{dialog}
|
||||||
|
</div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
@ -132,9 +134,10 @@ export default function InviteUserForm({
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">{t('close')}</Button>
|
<Button variant="outline">{t("close")}</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
type="submit"
|
type="submit"
|
||||||
form="confirm-delete-form"
|
form="confirm-delete-form"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function CopyTextBox({
|
||||||
>
|
>
|
||||||
<pre
|
<pre
|
||||||
ref={textRef}
|
ref={textRef}
|
||||||
className={`p-2 pr-16 text-sm w-full ${
|
className={`p-4 pr-16 text-sm w-full ${
|
||||||
wrapText
|
wrapText
|
||||||
? "whitespace-pre-wrap break-words"
|
? "whitespace-pre-wrap break-words"
|
||||||
: "overflow-x-auto"
|
: "overflow-x-auto"
|
||||||
|
|
|
@ -32,7 +32,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
|
||||||
href={text}
|
href={text}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="truncate hover:underline"
|
className="truncate hover:underline text-sm"
|
||||||
style={{ maxWidth: "100%" }} // Ensures truncation works within parent
|
style={{ maxWidth: "100%" }} // Ensures truncation works within parent
|
||||||
title={text} // Shows full text on hover
|
title={text} // Shows full text on hover
|
||||||
>
|
>
|
||||||
|
@ -40,7 +40,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
className="truncate"
|
className="truncate text-sm"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
display: "block",
|
display: "block",
|
||||||
|
|
|
@ -1,276 +1,78 @@
|
||||||
"use client";
|
import React from "react";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { SidebarNav } from "@app/components/SidebarNav";
|
|
||||||
import { OrgSelector } from "@app/components/OrgSelector";
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
import SupporterStatus from "@app/components/SupporterStatus";
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { LayoutSidebar } from "@app/components/LayoutSidebar";
|
||||||
import { ExternalLink, Menu, X, Server } from "lucide-react";
|
import { LayoutHeader } from "@app/components/LayoutHeader";
|
||||||
import Image from "next/image";
|
import { LayoutMobileMenu } from "@app/components/LayoutMobileMenu";
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
import { cookies } from "next/headers";
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription
|
|
||||||
} from "@app/components/ui/sheet";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { Breadcrumbs } from "@app/components/Breadcrumbs";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
orgs?: ListUserOrgsResponse["orgs"];
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
navItems?: Array<{
|
navItems?: SidebarNavSection[];
|
||||||
title: string;
|
|
||||||
href: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
children?: Array<{
|
|
||||||
title: string;
|
|
||||||
href: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
showSidebar?: boolean;
|
showSidebar?: boolean;
|
||||||
showBreadcrumbs?: boolean;
|
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
showTopBar?: boolean;
|
showTopBar?: boolean;
|
||||||
|
defaultSidebarCollapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({
|
export async function Layout({
|
||||||
children,
|
children,
|
||||||
orgId,
|
orgId,
|
||||||
orgs,
|
orgs,
|
||||||
navItems = [],
|
navItems = [],
|
||||||
showSidebar = true,
|
showSidebar = true,
|
||||||
showBreadcrumbs = true,
|
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
showTopBar = true
|
showTopBar = true,
|
||||||
|
defaultSidebarCollapsed = false
|
||||||
}: LayoutProps) {
|
}: LayoutProps) {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const allCookies = await cookies();
|
||||||
const { env } = useEnvContext();
|
const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value;
|
||||||
const pathname = usePathname();
|
|
||||||
const isAdminPage = pathname?.startsWith("/admin");
|
|
||||||
const { user } = useUserContext();
|
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const initialSidebarCollapsed = sidebarStateCookie === "collapsed" ||
|
||||||
const [path, setPath] = useState<string>(""); // Default logo path
|
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function getPath() {
|
|
||||||
let lightOrDark = theme;
|
|
||||||
|
|
||||||
if (theme === "system" || !theme) {
|
|
||||||
lightOrDark = window.matchMedia("(prefers-color-scheme: dark)")
|
|
||||||
.matches
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lightOrDark === "light") {
|
|
||||||
// return "/logo/word_mark_black.png";
|
|
||||||
return "/logo/pangolin_orange.svg";
|
|
||||||
}
|
|
||||||
|
|
||||||
// return "/logo/word_mark_white.png";
|
|
||||||
return "/logo/pangolin_orange.svg";
|
|
||||||
}
|
|
||||||
|
|
||||||
setPath(getPath());
|
|
||||||
}, [theme, env]);
|
|
||||||
|
|
||||||
const t = useTranslations();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen overflow-hidden">
|
<div className="flex h-screen overflow-hidden">
|
||||||
{/* Full width header */}
|
{/* Desktop Sidebar */}
|
||||||
{showHeader && (
|
{showSidebar && (
|
||||||
<div className="border-b shrink-0 bg-card">
|
<LayoutSidebar
|
||||||
<div className="h-16 flex items-center px-4">
|
orgId={orgId}
|
||||||
<div className="flex items-center gap-4">
|
orgs={orgs}
|
||||||
{showSidebar && (
|
navItems={navItems}
|
||||||
<div className="md:hidden">
|
defaultSidebarCollapsed={initialSidebarCollapsed}
|
||||||
<Sheet
|
/>
|
||||||
open={isMobileMenuOpen}
|
|
||||||
onOpenChange={setIsMobileMenuOpen}
|
|
||||||
>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<Menu className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent
|
|
||||||
side="left"
|
|
||||||
className="w-64 p-0 flex flex-col h-full"
|
|
||||||
>
|
|
||||||
<SheetTitle className="sr-only">
|
|
||||||
{t('navbar')}
|
|
||||||
</SheetTitle>
|
|
||||||
<SheetDescription className="sr-only">
|
|
||||||
{t('navbarDescription')}
|
|
||||||
</SheetDescription>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<div className="p-4">
|
|
||||||
<SidebarNav
|
|
||||||
items={navItems}
|
|
||||||
onItemClick={() =>
|
|
||||||
setIsMobileMenuOpen(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!isAdminPage &&
|
|
||||||
user.serverAdmin && (
|
|
||||||
<div className="p-4 border-t">
|
|
||||||
<Link
|
|
||||||
href="/admin"
|
|
||||||
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
|
|
||||||
onClick={() =>
|
|
||||||
setIsMobileMenuOpen(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Server className="h-4 w-4" />
|
|
||||||
{t('serverAdmin')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4 border-t shrink-0">
|
|
||||||
<SupporterStatus />
|
|
||||||
<OrgSelector
|
|
||||||
orgId={orgId}
|
|
||||||
orgs={orgs}
|
|
||||||
/>
|
|
||||||
{env?.app?.version && (
|
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
|
||||||
v{env.app.version}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="flex items-center hidden md:block"
|
|
||||||
>
|
|
||||||
{path && (
|
|
||||||
<Image
|
|
||||||
src={path}
|
|
||||||
alt="Pangolin Logo"
|
|
||||||
width={35}
|
|
||||||
height={35}
|
|
||||||
priority={true}
|
|
||||||
quality={25}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
{showBreadcrumbs && (
|
|
||||||
<div className="hidden md:block overflow-x-auto scrollbar-hide">
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showTopBar && (
|
|
||||||
<div className="ml-auto flex items-center justify-end md:justify-between">
|
|
||||||
<div className="hidden md:flex items-center space-x-3 mr-6">
|
|
||||||
<Link
|
|
||||||
href="https://docs.fossorial.io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
{t('navbarDocsLink')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ProfileIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showBreadcrumbs && (
|
|
||||||
<div className="md:hidden px-4 pb-2 overflow-x-auto scrollbar-hide">
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
{/* Main content area */}
|
||||||
{/* Desktop Sidebar */}
|
<div
|
||||||
{showSidebar && (
|
className={cn(
|
||||||
<div className="hidden md:flex w-64 border-r bg-card flex-col h-full shrink-0">
|
"flex-1 flex flex-col h-full min-w-0",
|
||||||
<div className="flex-1 overflow-y-auto">
|
!showSidebar && "w-full"
|
||||||
<div className="p-4">
|
)}
|
||||||
<SidebarNav items={navItems} />
|
>
|
||||||
</div>
|
{/* Mobile header */}
|
||||||
{!isAdminPage && user.serverAdmin && (
|
{showHeader && (
|
||||||
<div className="p-4 border-t">
|
<LayoutMobileMenu
|
||||||
<Link
|
orgId={orgId}
|
||||||
href="/admin"
|
orgs={orgs}
|
||||||
className="flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md w-full"
|
navItems={navItems}
|
||||||
>
|
showSidebar={showSidebar}
|
||||||
<Server className="h-4 w-4" />
|
showTopBar={showTopBar}
|
||||||
{t('serverAdmin')}
|
/>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4 border-t shrink-0">
|
|
||||||
<SupporterStatus />
|
|
||||||
<OrgSelector orgId={orgId} orgs={orgs} />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
|
||||||
<Link
|
|
||||||
href="https://github.com/fosrl/pangolin"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center justify-center gap-1"
|
|
||||||
>
|
|
||||||
{!isUnlocked()
|
|
||||||
? t('communityEdition')
|
|
||||||
: t('commercialEdition')}
|
|
||||||
<ExternalLink size={12} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{env?.app?.version && (
|
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
|
||||||
v{env.app.version}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Desktop header */}
|
||||||
|
{showHeader && <LayoutHeader showTopBar={showTopBar} />}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div
|
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
||||||
className={cn(
|
<div className="container mx-auto max-w-12xl mb-12">
|
||||||
"flex-1 flex flex-col h-full min-w-0",
|
{children}
|
||||||
!showSidebar && "w-full"
|
</div>
|
||||||
)}
|
</main>
|
||||||
>
|
|
||||||
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
|
||||||
<div className="container mx-auto max-w-12xl mb-12">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
73
src/components/LayoutHeader.tsx
Normal file
73
src/components/LayoutHeader.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
interface LayoutHeaderProps {
|
||||||
|
showTopBar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [path, setPath] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function getPath() {
|
||||||
|
let lightOrDark = theme;
|
||||||
|
|
||||||
|
if (theme === "system" || !theme) {
|
||||||
|
lightOrDark = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightOrDark === "light") {
|
||||||
|
return "/logo/word_mark_black.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/logo/word_mark_white.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
setPath(getPath());
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shrink-0 hidden md:block">
|
||||||
|
<div className="px-6 py-2">
|
||||||
|
<div className="container mx-auto max-w-12xl">
|
||||||
|
<div className="h-16 flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link href="/" className="flex items-center">
|
||||||
|
{path && (
|
||||||
|
<Image
|
||||||
|
src={path}
|
||||||
|
alt="Pangolin"
|
||||||
|
width={98}
|
||||||
|
height={32}
|
||||||
|
className="h-8 w-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile controls on the right */}
|
||||||
|
{showTopBar && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
<ProfileIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutHeader;
|
142
src/components/LayoutMobileMenu.tsx
Normal file
142
src/components/LayoutMobileMenu.tsx
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { SidebarNav } from "@app/components/SidebarNav";
|
||||||
|
import { OrgSelector } from "@app/components/OrgSelector";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
import SupporterStatus from "@app/components/SupporterStatus";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { Menu, Server } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription
|
||||||
|
} from "@app/components/ui/sheet";
|
||||||
|
import { Abel } from "next/font/google";
|
||||||
|
|
||||||
|
interface LayoutMobileMenuProps {
|
||||||
|
orgId?: string;
|
||||||
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
|
navItems: SidebarNavSection[];
|
||||||
|
showSidebar: boolean;
|
||||||
|
showTopBar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LayoutMobileMenu({
|
||||||
|
orgId,
|
||||||
|
orgs,
|
||||||
|
navItems,
|
||||||
|
showSidebar,
|
||||||
|
showTopBar
|
||||||
|
}: LayoutMobileMenuProps) {
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isAdminPage = pathname?.startsWith("/admin");
|
||||||
|
const { user } = useUserContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shrink-0 md:hidden">
|
||||||
|
<div className="h-16 flex items-center px-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{showSidebar && (
|
||||||
|
<div>
|
||||||
|
<Sheet
|
||||||
|
open={isMobileMenuOpen}
|
||||||
|
onOpenChange={setIsMobileMenuOpen}
|
||||||
|
>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Menu className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent
|
||||||
|
side="left"
|
||||||
|
className="w-64 p-0 flex flex-col h-full"
|
||||||
|
>
|
||||||
|
<SheetTitle className="sr-only">
|
||||||
|
{t("navbar")}
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription className="sr-only">
|
||||||
|
{t("navbarDescription")}
|
||||||
|
</SheetDescription>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="p-4">
|
||||||
|
<OrgSelector
|
||||||
|
orgId={orgId}
|
||||||
|
orgs={orgs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-4">
|
||||||
|
{!isAdminPage &&
|
||||||
|
user.serverAdmin && (
|
||||||
|
<div className="pb-3">
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md px-3 py-1.5"
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
setIsMobileMenuOpen(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 mr-2">
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
"serverAdmin"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SidebarNav
|
||||||
|
sections={navItems}
|
||||||
|
onItemClick={() =>
|
||||||
|
setIsMobileMenuOpen(false)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-4 border-t shrink-0">
|
||||||
|
<SupporterStatus />
|
||||||
|
{env?.app?.version && (
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
v{env.app.version}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showTopBar && (
|
||||||
|
<div className="ml-auto flex items-center justify-end">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
<ProfileIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutMobileMenu;
|
178
src/components/LayoutSidebar.tsx
Normal file
178
src/components/LayoutSidebar.tsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { SidebarNav } from "@app/components/SidebarNav";
|
||||||
|
import { OrgSelector } from "@app/components/OrgSelector";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
import SupporterStatus from "@app/components/SupporterStatus";
|
||||||
|
import { ExternalLink, Server, BookOpenText } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
|
|
||||||
|
interface LayoutSidebarProps {
|
||||||
|
orgId?: string;
|
||||||
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
|
navItems: SidebarNavSection[];
|
||||||
|
defaultSidebarCollapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LayoutSidebar({
|
||||||
|
orgId,
|
||||||
|
orgs,
|
||||||
|
navItems,
|
||||||
|
defaultSidebarCollapsed
|
||||||
|
}: LayoutSidebarProps) {
|
||||||
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(
|
||||||
|
defaultSidebarCollapsed
|
||||||
|
);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isAdminPage = pathname?.startsWith("/admin");
|
||||||
|
const { user } = useUserContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const setSidebarStateCookie = (collapsed: boolean) => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const isSecure = window.location.protocol === "https:";
|
||||||
|
document.cookie = `pangolin-sidebar-state=${collapsed ? "collapsed" : "expanded"}; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax${isSecure ? "; secure" : ""}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSidebarStateCookie(isSidebarCollapsed);
|
||||||
|
}, [isSidebarCollapsed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"hidden md:flex border-r bg-card flex-col h-full shrink-0 relative",
|
||||||
|
isSidebarCollapsed ? "w-16" : "w-64"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="p-4">
|
||||||
|
<OrgSelector
|
||||||
|
orgId={orgId}
|
||||||
|
orgs={orgs}
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pt-1">
|
||||||
|
{!isAdminPage && user.serverAdmin && (
|
||||||
|
<div className="pb-4">
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
|
||||||
|
isSidebarCollapsed
|
||||||
|
? "px-2 py-2 justify-center"
|
||||||
|
: "px-3 py-1.5"
|
||||||
|
)}
|
||||||
|
title={
|
||||||
|
isSidebarCollapsed
|
||||||
|
? t("serverAdmin")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex-shrink-0",
|
||||||
|
!isSidebarCollapsed && "mr-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
{!isSidebarCollapsed && (
|
||||||
|
<span>{t("serverAdmin")}</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SidebarNav
|
||||||
|
sections={navItems}
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-4 border-t shrink-0">
|
||||||
|
<SupporterStatus isCollapsed={isSidebarCollapsed} />
|
||||||
|
{!isSidebarCollapsed && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
<Link
|
||||||
|
href="https://github.com/fosrl/pangolin"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
{!isUnlocked()
|
||||||
|
? t("communityEdition")
|
||||||
|
: t("commercialEdition")}
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground ">
|
||||||
|
<Link
|
||||||
|
href="https://docs.fossorial.io/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
{t("documentation")}
|
||||||
|
<BookOpenText size={12} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{env?.app?.version && (
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
v{env.app.version}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapse button */}
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setIsSidebarCollapsed(!isSidebarCollapsed)
|
||||||
|
}
|
||||||
|
className="cursor-pointer absolute -right-2.5 top-1/2 transform -translate-y-1/2 w-2 h-8 rounded-full flex items-center justify-center transition-all duration-200 ease-in-out hover:scale-110 group"
|
||||||
|
aria-label={
|
||||||
|
isSidebarCollapsed
|
||||||
|
? "Expand sidebar"
|
||||||
|
: "Collapse sidebar"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-0.5 h-4 bg-current opacity-30 group-hover:opacity-100 transition-opacity duration-200" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
<p>
|
||||||
|
{isSidebarCollapsed
|
||||||
|
? t("sidebarExpand")
|
||||||
|
: t("sidebarCollapse")}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutSidebar;
|
|
@ -1,55 +1,55 @@
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale } from "next-intl";
|
||||||
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
|
import LocaleSwitcherSelect from "./LocaleSwitcherSelect";
|
||||||
|
|
||||||
export default function LocaleSwitcher() {
|
export default function LocaleSwitcher() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleSwitcherSelect
|
<LocaleSwitcherSelect
|
||||||
label="Select language"
|
label="Select language"
|
||||||
defaultValue={locale}
|
defaultValue={locale}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
value: 'en-US',
|
value: "en-US",
|
||||||
label: 'English'
|
label: "English"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'fr-FR',
|
value: "fr-FR",
|
||||||
label: "Français"
|
label: "Français"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'de-DE',
|
value: "de-DE",
|
||||||
label: 'Deutsch'
|
label: "Deutsch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'it-IT',
|
value: "it-IT",
|
||||||
label: 'Italiano'
|
label: "Italiano"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'nl-NL',
|
value: "nl-NL",
|
||||||
label: 'Nederlands'
|
label: "Nederlands"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'pl-PL',
|
value: "pl-PL",
|
||||||
label: 'Polski'
|
label: "Polski"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'pt-PT',
|
value: "pt-PT",
|
||||||
label: 'Português'
|
label: "Português"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'es-ES',
|
value: "es-ES",
|
||||||
label: 'Español'
|
label: "Español"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'tr-TR',
|
value: "tr-TR",
|
||||||
label: 'Türkçe'
|
label: "Türkçe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'zh-CN',
|
value: "zh-CN",
|
||||||
label: '简体中文'
|
label: "简体中文"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +1,71 @@
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@app/components/ui/dropdown-menu';
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { Button } from '@app/components/ui/button';
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Check, Globe, Languages } from 'lucide-react';
|
import { Check, Globe, Languages } from "lucide-react";
|
||||||
import clsx from 'clsx';
|
import clsx from "clsx";
|
||||||
import { useTransition } from 'react';
|
import { useTransition } from "react";
|
||||||
import { Locale } from '@/i18n/config';
|
import { Locale } from "@/i18n/config";
|
||||||
import { setUserLocale } from '@/services/locale';
|
import { setUserLocale } from "@/services/locale";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
items: Array<{ value: string; label: string }>;
|
items: Array<{ value: string; label: string }>;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LocaleSwitcherSelect({
|
export default function LocaleSwitcherSelect({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
items,
|
items,
|
||||||
label
|
label
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
function onChange(value: string) {
|
function onChange(value: string) {
|
||||||
const locale = value as Locale;
|
const locale = value as Locale;
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setUserLocale(locale);
|
setUserLocale(locale);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = items.find((item) => item.value === defaultValue);
|
const selected = items.find((item) => item.value === defaultValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full rounded-sm h-8 gap-2 justify-start font-normal',
|
"w-full rounded-sm h-8 gap-2 justify-start font-normal",
|
||||||
isPending && 'pointer-events-none'
|
isPending && "pointer-events-none"
|
||||||
)}
|
)}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<Languages className="h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
<span className="text-left flex-1">
|
<span className="text-left flex-1">
|
||||||
{selected?.label ?? label}
|
{selected?.label ?? label}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="min-w-[8rem]">
|
<DropdownMenuContent align="end" className="min-w-[8rem]">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={item.value}
|
key={item.value}
|
||||||
onClick={() => onChange(item.value)}
|
onClick={() => onChange(item.value)}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{item.value === defaultValue && (
|
{item.value === defaultValue && (
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,16 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
import { Check, ChevronsUpDown, Plus, Building2, Users } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
@ -27,94 +33,110 @@ import { useTranslations } from "next-intl";
|
||||||
interface OrgSelectorProps {
|
interface OrgSelectorProps {
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
orgs?: ListUserOrgsResponse["orgs"];
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
|
isCollapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OrgSelector({ orgId, orgs }: OrgSelectorProps) {
|
export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorProps) {
|
||||||
const { user } = useUserContext();
|
const { user } = useUserContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
const selectedOrg = orgs?.find((org) => org.orgId === orgId);
|
||||||
|
|
||||||
|
const orgSelectorContent = (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
size="lg"
|
size={isCollapsed ? "icon" : "lg"}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-full h-12 px-3 py-4 bg-neutral hover:bg-neutral"
|
className={cn(
|
||||||
|
"shadow-xs",
|
||||||
|
isCollapsed ? "w-8 h-8" : "w-full h-12 px-3 py-4"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full min-w-0">
|
{isCollapsed ? (
|
||||||
<div className="flex flex-col items-start min-w-0 flex-1">
|
<Building2 className="h-4 w-4" />
|
||||||
<span className="font-bold text-sm">
|
) : (
|
||||||
{t('org')}
|
<div className="flex items-center justify-between w-full min-w-0">
|
||||||
</span>
|
<div className="flex items-center min-w-0 flex-1">
|
||||||
<span className="text-sm text-muted-foreground truncate w-full">
|
<Building2 className="h-4 w-4 mr-2 shrink-0" />
|
||||||
{orgId
|
<div className="flex flex-col items-start min-w-0 flex-1">
|
||||||
? orgs?.find(
|
<span className="font-bold text-sm">
|
||||||
(org) =>
|
{t('org')}
|
||||||
org.orgId ===
|
</span>
|
||||||
orgId
|
<span className="text-sm text-muted-foreground truncate w-full text-left">
|
||||||
)?.name
|
{selectedOrg?.name || t('noneSelected')}
|
||||||
: t('noneSelected')}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 ml-2" />
|
)}
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[280px] p-0">
|
<PopoverContent className="w-[320px] p-0" align="start">
|
||||||
<Command>
|
<Command className="rounded-lg">
|
||||||
<CommandInput placeholder={t('searchProgress')} />
|
<CommandInput
|
||||||
<CommandEmpty>
|
placeholder={t('searchProgress')}
|
||||||
{t('orgNotFound2')}
|
className="border-0 focus:ring-0"
|
||||||
|
/>
|
||||||
|
<CommandEmpty className="py-6 text-center">
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
{t('orgNotFound2')}
|
||||||
|
</div>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
{(!env.flags.disableUserCreateOrg ||
|
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
|
||||||
user.serverAdmin) && (
|
|
||||||
<>
|
<>
|
||||||
<CommandGroup heading={t('create')}>
|
<CommandGroup heading={t('create')} className="py-2">
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={(
|
onSelect={() => {
|
||||||
currentValue
|
setOpen(false);
|
||||||
) => {
|
router.push("/setup");
|
||||||
router.push(
|
|
||||||
"/setup"
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
className="mx-2 rounded-md"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
||||||
{t('setupNewOrg')}
|
<Plus className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{t('setupNewOrg')}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">Create a new organization</span>
|
||||||
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator className="my-2" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CommandGroup heading={t('orgs')}>
|
<CommandGroup heading={t('orgs')} className="py-2">
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{orgs?.map((org) => (
|
{orgs?.map((org) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={org.orgId}
|
key={org.orgId}
|
||||||
onSelect={(
|
onSelect={() => {
|
||||||
currentValue
|
setOpen(false);
|
||||||
) => {
|
router.push(`/${org.orgId}/settings`);
|
||||||
router.push(
|
|
||||||
`/${org.orgId}/settings`
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
className="mx-2 rounded-md"
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted mr-3">
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<span className="font-medium">{org.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">Organization</span>
|
||||||
|
</div>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"h-4 w-4 text-primary",
|
||||||
orgId === org.orgId
|
orgId === org.orgId ? "opacity-100" : "opacity-0"
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{org.name}
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
@ -123,4 +145,24 @@ export function OrgSelector({ orgId, orgs }: OrgSelectorProps) {
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
{orgSelectorContent}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="font-medium">{selectedOrg?.name || t('noneSelected')}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{t('org')}</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgSelectorContent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,9 @@ import Disable2FaForm from "./Disable2FaForm";
|
||||||
import Enable2FaForm from "./Enable2FaForm";
|
import Enable2FaForm from "./Enable2FaForm";
|
||||||
import SupporterStatus from "./SupporterStatus";
|
import SupporterStatus from "./SupporterStatus";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import LocaleSwitcher from '@app/components/LocaleSwitcher';
|
import LocaleSwitcher from "@app/components/LocaleSwitcher";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
|
||||||
export default function ProfileIcon() {
|
export default function ProfileIcon() {
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
@ -57,10 +56,10 @@ export default function ProfileIcon() {
|
||||||
function logout() {
|
function logout() {
|
||||||
api.post("/auth/logout")
|
api.post("/auth/logout")
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(t('logoutError'), e);
|
console.error(t("logoutError"), e);
|
||||||
toast({
|
toast({
|
||||||
title: t('logoutError'),
|
title: t("logoutError"),
|
||||||
description: formatAxiosError(e, t('logoutError'))
|
description: formatAxiosError(e, t("logoutError"))
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -74,104 +73,93 @@ export default function ProfileIcon() {
|
||||||
<Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
|
<Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
|
||||||
<Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} />
|
<Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} />
|
||||||
|
|
||||||
<div className="flex items-center md:gap-2 grow min-w-0 gap-2 md:gap-0">
|
<DropdownMenu>
|
||||||
<span className="truncate max-w-full font-medium min-w-0">
|
<DropdownMenuTrigger asChild>
|
||||||
{user.email || user.name || user.username}
|
<Button
|
||||||
</span>
|
variant="outline"
|
||||||
<DropdownMenu>
|
className="relative h-10 w-10 rounded-full"
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="relative h-10 w-10 rounded-full"
|
|
||||||
>
|
|
||||||
<Avatar className="h-9 w-9">
|
|
||||||
<AvatarFallback>{getInitials()}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-56"
|
|
||||||
align="start"
|
|
||||||
forceMount
|
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<Avatar className="h-9 w-9">
|
||||||
<div className="flex flex-col space-y-1">
|
<AvatarFallback>{getInitials()}</AvatarFallback>
|
||||||
<p className="text-sm font-medium leading-none">
|
</Avatar>
|
||||||
{t('signingAs')}
|
</Button>
|
||||||
</p>
|
</DropdownMenuTrigger>
|
||||||
<p className="text-xs leading-none text-muted-foreground">
|
<DropdownMenuContent className="w-56" align="start" forceMount>
|
||||||
{user.email || user.name || user.username}
|
<DropdownMenuLabel className="font-normal">
|
||||||
</p>
|
<div className="flex flex-col space-y-1">
|
||||||
</div>
|
<p className="text-sm font-medium leading-none">
|
||||||
{user.serverAdmin ? (
|
{t("signingAs")}
|
||||||
<p className="text-xs leading-none text-muted-foreground mt-2">
|
</p>
|
||||||
{t('serverAdmin')}
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
</p>
|
{user.email || user.name || user.username}
|
||||||
) : (
|
</p>
|
||||||
<p className="text-xs leading-none text-muted-foreground mt-2">
|
</div>
|
||||||
{user.idpName || t('idpNameInternal')}
|
{user.serverAdmin ? (
|
||||||
</p>
|
<p className="text-xs leading-none text-muted-foreground mt-2">
|
||||||
)}
|
{t("serverAdmin")}
|
||||||
</DropdownMenuLabel>
|
</p>
|
||||||
<DropdownMenuSeparator />
|
) : (
|
||||||
{user?.type === UserType.Internal && (
|
<p className="text-xs leading-none text-muted-foreground mt-2">
|
||||||
<>
|
{user.idpName || t("idpNameInternal")}
|
||||||
{!user.twoFactorEnabled && (
|
</p>
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setOpenEnable2fa(true)}
|
|
||||||
>
|
|
||||||
<span>{t('otpEnable')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{user.twoFactorEnabled && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setOpenDisable2fa(true)}
|
|
||||||
>
|
|
||||||
<span>{t('otpDisable')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<DropdownMenuLabel>{t("theme")}</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{(["light", "dark", "system"] as const).map(
|
<DropdownMenuSeparator />
|
||||||
(themeOption) => (
|
{user?.type === UserType.Internal && (
|
||||||
|
<>
|
||||||
|
{!user.twoFactorEnabled && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={themeOption}
|
onClick={() => setOpenEnable2fa(true)}
|
||||||
onClick={() =>
|
|
||||||
handleThemeChange(themeOption)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{themeOption === "light" && (
|
<span>{t("otpEnable")}</span>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{themeOption === "dark" && (
|
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{themeOption === "system" && (
|
|
||||||
<Laptop className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
<span className="capitalize">
|
|
||||||
{t(themeOption)}
|
|
||||||
</span>
|
|
||||||
{userTheme === themeOption && (
|
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-primary"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)
|
)}
|
||||||
)}
|
{user.twoFactorEnabled && (
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem
|
||||||
<LocaleSwitcher />
|
onClick={() => setOpenDisable2fa(true)}
|
||||||
<DropdownMenuSeparator />
|
>
|
||||||
<DropdownMenuItem onClick={() => logout()}>
|
<span>{t("otpDisable")}</span>
|
||||||
{/* <LogOut className="mr-2 h-4 w-4" /> */}
|
</DropdownMenuItem>
|
||||||
<span>{t('logout')}</span>
|
)}
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuContent>
|
</>
|
||||||
</DropdownMenu>
|
)}
|
||||||
</div>
|
<DropdownMenuLabel>{t("theme")}</DropdownMenuLabel>
|
||||||
|
{(["light", "dark", "system"] as const).map(
|
||||||
|
(themeOption) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={themeOption}
|
||||||
|
onClick={() => handleThemeChange(themeOption)}
|
||||||
|
>
|
||||||
|
{themeOption === "light" && (
|
||||||
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{themeOption === "dark" && (
|
||||||
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{themeOption === "system" && (
|
||||||
|
<Laptop className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="capitalize">
|
||||||
|
{t(themeOption)}
|
||||||
|
</span>
|
||||||
|
{userTheme === themeOption && (
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-primary"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<LocaleSwitcher />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
|
{/* <LogOut className="mr-2 h-4 w-4" /> */}
|
||||||
|
<span>{t("logout")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
|
|
||||||
export interface SidebarNavItem {
|
export type SidebarNavItem = {
|
||||||
href: string;
|
href: string;
|
||||||
title: string;
|
title: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
children?: SidebarNavItem[];
|
|
||||||
autoExpand?: boolean;
|
|
||||||
showProfessional?: boolean;
|
showProfessional?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type SidebarNavSection = {
|
||||||
|
heading: string;
|
||||||
|
items: SidebarNavItem[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
items: SidebarNavItem[];
|
sections: SidebarNavSection[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onItemClick?: () => void;
|
onItemClick?: () => void;
|
||||||
|
isCollapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarNav({
|
export function SidebarNav({
|
||||||
className,
|
className,
|
||||||
items,
|
sections,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
|
isCollapsed = false,
|
||||||
...props
|
...props
|
||||||
}: SidebarNavProps) {
|
}: SidebarNavProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
@ -39,34 +49,8 @@ export function SidebarNav({
|
||||||
const resourceId = params.resourceId as string;
|
const resourceId = params.resourceId as string;
|
||||||
const userId = params.userId as string;
|
const userId = params.userId as string;
|
||||||
const clientId = params.clientId as string;
|
const clientId = params.clientId as string;
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {
|
|
||||||
const autoExpanded = new Set<string>();
|
|
||||||
|
|
||||||
function findAutoExpandedAndActivePath(
|
|
||||||
items: SidebarNavItem[],
|
|
||||||
parentHrefs: string[] = []
|
|
||||||
) {
|
|
||||||
items.forEach((item) => {
|
|
||||||
const hydratedHref = hydrateHref(item.href);
|
|
||||||
const currentPath = [...parentHrefs, hydratedHref];
|
|
||||||
|
|
||||||
if (item.autoExpand || pathname.startsWith(hydratedHref)) {
|
|
||||||
currentPath.forEach((href) => autoExpanded.add(href));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.children) {
|
|
||||||
findAutoExpandedAndActivePath(item.children, currentPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAutoExpandedAndActivePath(items);
|
|
||||||
return autoExpanded;
|
|
||||||
});
|
|
||||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
const { user } = useUserContext();
|
const { user } = useUserContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
function hydrateHref(val: string): string {
|
function hydrateHref(val: string): string {
|
||||||
|
@ -78,116 +62,109 @@ export function SidebarNav({
|
||||||
.replace("{clientId}", clientId);
|
.replace("{clientId}", clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleItem(href: string) {
|
const renderNavItem = (
|
||||||
setExpandedItems((prev) => {
|
item: SidebarNavItem,
|
||||||
const newSet = new Set(prev);
|
hydratedHref: string,
|
||||||
if (newSet.has(href)) {
|
isActive: boolean,
|
||||||
newSet.delete(href);
|
isDisabled: boolean
|
||||||
} else {
|
) => {
|
||||||
newSet.add(href);
|
const tooltipText =
|
||||||
}
|
item.showProfessional && !isUnlocked()
|
||||||
return newSet;
|
? `${t(item.title)} (${t("licenseBadge")})`
|
||||||
});
|
: t(item.title);
|
||||||
}
|
|
||||||
|
|
||||||
function renderItems(items: SidebarNavItem[], level = 0) {
|
const itemContent = (
|
||||||
return items.map((item) => {
|
<Link
|
||||||
const hydratedHref = hydrateHref(item.href);
|
href={isDisabled ? "#" : hydratedHref}
|
||||||
const isActive = pathname.startsWith(hydratedHref);
|
className={cn(
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
"flex items-center rounded transition-colors hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
|
||||||
const isExpanded = expandedItems.has(hydratedHref);
|
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
|
||||||
const indent = level * 28; // Base indent for each level
|
isActive
|
||||||
const isProfessional = item.showProfessional && !isUnlocked();
|
? "text-primary font-medium"
|
||||||
const isDisabled = disabled || isProfessional;
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
isDisabled && "cursor-not-allowed opacity-60"
|
||||||
return (
|
)}
|
||||||
<div key={hydratedHref}>
|
onClick={(e) => {
|
||||||
<div
|
if (isDisabled) {
|
||||||
className="flex items-center group"
|
e.preventDefault();
|
||||||
style={{ marginLeft: `${indent}px` }}
|
} else if (onItemClick) {
|
||||||
|
onItemClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={isDisabled ? -1 : undefined}
|
||||||
|
aria-disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{item.icon && (
|
||||||
|
<span
|
||||||
|
className={cn("flex-shrink-0", !isCollapsed && "mr-2")}
|
||||||
>
|
>
|
||||||
<div
|
{item.icon}
|
||||||
className={cn(
|
</span>
|
||||||
"flex items-center w-full transition-colors rounded-md",
|
)}
|
||||||
isActive && level === 0 && "bg-primary/10"
|
{!isCollapsed && (
|
||||||
)}
|
<>
|
||||||
>
|
<span>{t(item.title)}</span>
|
||||||
<Link
|
{item.showProfessional && !isUnlocked() && (
|
||||||
href={isProfessional ? "#" : hydratedHref}
|
<Badge variant="outlinePrimary" className="ml-2">
|
||||||
className={cn(
|
{t("licenseBadge")}
|
||||||
"flex items-center w-full px-3 py-2",
|
</Badge>
|
||||||
isActive
|
)}
|
||||||
? "text-primary font-medium"
|
</>
|
||||||
: "text-muted-foreground group-hover:text-foreground",
|
)}
|
||||||
isDisabled && "cursor-not-allowed"
|
</Link>
|
||||||
)}
|
);
|
||||||
onClick={(e) => {
|
|
||||||
if (isDisabled) {
|
if (isCollapsed) {
|
||||||
e.preventDefault();
|
return (
|
||||||
} else if (onItemClick) {
|
<TooltipProvider key={hydratedHref}>
|
||||||
onItemClick();
|
<Tooltip>
|
||||||
}
|
<TooltipTrigger asChild>{itemContent}</TooltipTrigger>
|
||||||
}}
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
tabIndex={isDisabled ? -1 : undefined}
|
<p>{tooltipText}</p>
|
||||||
aria-disabled={isDisabled}
|
</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
<div
|
</TooltipProvider>
|
||||||
className={cn(
|
|
||||||
"flex items-center",
|
|
||||||
isDisabled && "opacity-60"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.icon && (
|
|
||||||
<span className="mr-3">
|
|
||||||
{item.icon}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{t(item.title)}
|
|
||||||
</div>
|
|
||||||
{isProfessional && (
|
|
||||||
<Badge
|
|
||||||
variant="outlinePrimary"
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
{t('licenseBadge')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
{hasChildren && (
|
|
||||||
<button
|
|
||||||
onClick={() => toggleItem(hydratedHref)}
|
|
||||||
className="p-2 rounded-md text-muted-foreground hover:text-foreground cursor-pointer"
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronDown className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{hasChildren && isExpanded && (
|
|
||||||
<div className="space-y-1 mt-1">
|
|
||||||
{renderItems(item.children || [], level + 1)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={hydratedHref}>{itemContent}</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2",
|
"flex flex-col gap-2 text-sm",
|
||||||
disabled && "pointer-events-none opacity-60",
|
disabled && "pointer-events-none opacity-60",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{renderItems(items)}
|
{sections.map((section) => (
|
||||||
|
<div key={section.heading} className="mb-2">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
|
{section.heading}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{section.items.map((item) => {
|
||||||
|
const hydratedHref = hydrateHref(item.href);
|
||||||
|
const isActive = pathname.startsWith(hydratedHref);
|
||||||
|
const isProfessional =
|
||||||
|
item.showProfessional && !isUnlocked();
|
||||||
|
const isDisabled = disabled || isProfessional;
|
||||||
|
return renderNavItem(
|
||||||
|
item,
|
||||||
|
hydratedHref,
|
||||||
|
isActive,
|
||||||
|
isDisabled || false
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { SidebarNav } from "@app/components/SidebarNav";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface SideBarSettingsProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
sidebarNavItems: Array<{
|
|
||||||
title: string;
|
|
||||||
href: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}>;
|
|
||||||
disabled?: boolean;
|
|
||||||
limitWidth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SidebarSettings({
|
|
||||||
children,
|
|
||||||
sidebarNavItems,
|
|
||||||
disabled,
|
|
||||||
limitWidth
|
|
||||||
}: SideBarSettingsProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-6 lg:space-y-0">
|
|
||||||
<aside className="lg:w-1/5">
|
|
||||||
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
|
||||||
</aside>
|
|
||||||
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""} space-y-6`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -9,6 +9,12 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
|
@ -46,11 +52,15 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "./ui/card";
|
} from "./ui/card";
|
||||||
import { Check, ExternalLink } from "lucide-react";
|
import { Check, ExternalLink, Heart } from "lucide-react";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function SupporterStatus() {
|
interface SupporterStatusProps {
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SupporterStatus({ isCollapsed = false }: SupporterStatusProps) {
|
||||||
const { supporterStatus, updateSupporterStatus } =
|
const { supporterStatus, updateSupporterStatus } =
|
||||||
useSupporterStatusContext();
|
useSupporterStatusContext();
|
||||||
const [supportOpen, setSupportOpen] = useState(false);
|
const [supportOpen, setSupportOpen] = useState(false);
|
||||||
|
@ -411,16 +421,36 @@ export default function SupporterStatus() {
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
||||||
{supporterStatus?.visible ? (
|
{supporterStatus?.visible ? (
|
||||||
<Button
|
isCollapsed ? (
|
||||||
variant="outlinePrimary"
|
<TooltipProvider>
|
||||||
size="sm"
|
<Tooltip>
|
||||||
className="gap-2 w-full"
|
<TooltipTrigger asChild>
|
||||||
onClick={() => {
|
<Button
|
||||||
setPurchaseOptionsOpen(true);
|
size="icon"
|
||||||
}}
|
className="w-8 h-8"
|
||||||
>
|
onClick={() => {
|
||||||
{t('supportKeyBuy')}
|
setPurchaseOptionsOpen(true);
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
<Heart className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
<p>{t('supportKeyBuy')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setPurchaseOptionsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('supportKeyBuy')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
78
src/components/ThemeSwitcher.tsx
Normal file
78
src/components/ThemeSwitcher.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { Laptop, Moon, Sun } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function ThemeSwitcher() {
|
||||||
|
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="sm" className="h-8">
|
||||||
|
<Sun className="h-4 w-4 mr-2" />
|
||||||
|
Light
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleTheme() {
|
||||||
|
const currentTheme = theme || "system";
|
||||||
|
|
||||||
|
if (currentTheme === "light") {
|
||||||
|
setTheme("dark");
|
||||||
|
} else if (currentTheme === "dark") {
|
||||||
|
setTheme("system");
|
||||||
|
} else {
|
||||||
|
setTheme("light");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeIcon() {
|
||||||
|
const currentTheme = theme || "system";
|
||||||
|
|
||||||
|
if (currentTheme === "light") {
|
||||||
|
return <Sun className="h-4 w-4" />;
|
||||||
|
} else if (currentTheme === "dark") {
|
||||||
|
return <Moon className="h-4 w-4" />;
|
||||||
|
} else {
|
||||||
|
// When theme is "system", show icon based on resolved theme
|
||||||
|
if (resolvedTheme === "light") {
|
||||||
|
return <Sun className="h-4 w-4" />;
|
||||||
|
} else if (resolvedTheme === "dark") {
|
||||||
|
return <Moon className="h-4 w-4" />;
|
||||||
|
} else {
|
||||||
|
// Fallback to laptop icon if resolvedTheme is not available
|
||||||
|
return <Laptop className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeText() {
|
||||||
|
const currentTheme = theme || "system";
|
||||||
|
const translated = t(currentTheme);
|
||||||
|
return translated.charAt(0).toUpperCase() + translated.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8"
|
||||||
|
onClick={cycleTheme}
|
||||||
|
title={`Current theme: ${theme || "system"}. Click to cycle themes.`}
|
||||||
|
>
|
||||||
|
{getThemeIcon()}
|
||||||
|
<span className="ml-2">{getThemeText()}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -173,7 +173,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
(maxTags !== undefined && maxTags < 0) ||
|
(maxTags !== undefined && maxTags < 0) ||
|
||||||
(props.minTags !== undefined && props.minTags < 0)
|
(props.minTags !== undefined && props.minTags < 0)
|
||||||
) {
|
) {
|
||||||
console.warn(t('tagsWarnCannotBeLessThanZero'));
|
console.warn(t("tagsWarnCannotBeLessThanZero"));
|
||||||
// error
|
// error
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -197,22 +197,28 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
(option) => option.text === newTagText
|
(option) => option.text === newTagText
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
console.warn(t('tagsWarnNotAllowedAutocompleteOptions'));
|
console.warn(
|
||||||
|
t("tagsWarnNotAllowedAutocompleteOptions")
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateTag && !validateTag(newTagText)) {
|
if (validateTag && !validateTag(newTagText)) {
|
||||||
console.warn(t('tagsWarnInvalid'));
|
console.warn(t("tagsWarnInvalid"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minLength && newTagText.length < minLength) {
|
if (minLength && newTagText.length < minLength) {
|
||||||
console.warn(t('tagWarnTooShort', {tagText: newTagText}));
|
console.warn(
|
||||||
|
t("tagWarnTooShort", { tagText: newTagText })
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxLength && newTagText.length > maxLength) {
|
if (maxLength && newTagText.length > maxLength) {
|
||||||
console.warn(t('tagWarnTooLong', {tagText: newTagText}));
|
console.warn(
|
||||||
|
t("tagWarnTooLong", { tagText: newTagText })
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,10 +235,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
setTags((prevTags) => [...prevTags, newTag]);
|
setTags((prevTags) => [...prevTags, newTag]);
|
||||||
onTagAdd?.(newTagText);
|
onTagAdd?.(newTagText);
|
||||||
} else {
|
} else {
|
||||||
console.warn(t('tagsWarnReachedMaxNumber'));
|
console.warn(t("tagsWarnReachedMaxNumber"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(t('tagWarnDuplicate', {tagText: newTagText}));
|
console.warn(
|
||||||
|
t("tagWarnDuplicate", { tagText: newTagText })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
|
@ -258,12 +266,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minLength && newTagText.length < minLength) {
|
if (minLength && newTagText.length < minLength) {
|
||||||
console.warn(t('tagWarnTooShort'));
|
console.warn(t("tagWarnTooShort"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxLength && newTagText.length > maxLength) {
|
if (maxLength && newTagText.length > maxLength) {
|
||||||
console.warn(t('tagWarnTooLong'));
|
console.warn(t("tagWarnTooLong"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,7 +316,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minLength && newTagText.length < minLength) {
|
if (minLength && newTagText.length < minLength) {
|
||||||
console.warn(t('tagWarnTooShort'));
|
console.warn(t("tagWarnTooShort"));
|
||||||
// error
|
// error
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -316,7 +324,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
// Validate maxLength
|
// Validate maxLength
|
||||||
if (maxLength && newTagText.length > maxLength) {
|
if (maxLength && newTagText.length > maxLength) {
|
||||||
// error
|
// error
|
||||||
console.warn(t('tagWarnTooLong'));
|
console.warn(t("tagWarnTooLong"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,7 +497,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 bg-transparent`,
|
`flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50`,
|
||||||
styleClasses?.inlineTagsContainer
|
styleClasses?.inlineTagsContainer
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -536,7 +544,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
|
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
|
@ -622,7 +630,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
|
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
|
@ -710,7 +718,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
|
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
|
@ -791,7 +799,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit",
|
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
|
@ -834,7 +842,8 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
styleClasses?.input
|
styleClasses?.input,
|
||||||
|
"shadow-none inset-shadow-none"
|
||||||
// className
|
// className
|
||||||
)}
|
)}
|
||||||
autoComplete={
|
autoComplete={
|
||||||
|
@ -908,7 +917,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
tags.length >= maxTags)
|
tags.length >= maxTags)
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 w-full",
|
"border-0 w-full shadow-none inset-shadow-none",
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
// className
|
// className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -14,14 +14,13 @@ const alertVariants = cva(
|
||||||
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
success:
|
success:
|
||||||
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||||
info:
|
info: "border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500"
|
||||||
"border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500",
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default"
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
|
@ -45,7 +44,7 @@ const AlertTitle = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-1 font-medium leading-none tracking-tight",
|
"mb-1 font-medium leading-none tracking-tight",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,35 +6,35 @@ import { cn } from "@app/lib/cn";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"cursor-pointer inline-flex items-center justify-center rounded-full whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
|
"cursor-pointer inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-2xs",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 shadow-2xs",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-card hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-card hover:bg-accent hover:text-accent-foreground shadow-2xs",
|
||||||
outlinePrimary:
|
outlinePrimary:
|
||||||
"border border-primary bg-card hover:bg-primary/10 text-primary",
|
"border border-primary bg-card hover:bg-primary/10 text-primary shadow-2xs",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary border border-input border text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary border border-input border text-secondary-foreground hover:bg-secondary/80 shadow-2xs",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
squareOutlinePrimary:
|
squareOutlinePrimary:
|
||||||
"border border-primary bg-card hover:bg-primary/10 text-primary rounded-md",
|
"border border-primary bg-card hover:bg-primary/10 text-primary rounded-md shadow-2xs",
|
||||||
squareOutline:
|
squareOutline:
|
||||||
"border border-input bg-card hover:bg-accent hover:text-accent-foreground rounded-md",
|
"border border-input bg-card hover:bg-accent hover:text-accent-foreground rounded-md shadow-2xs",
|
||||||
squareDefault:
|
squareDefault:
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90 rounded-md",
|
"bg-primary text-primary-foreground hover:bg-primary/90 rounded-md shadow-2xs",
|
||||||
text: "",
|
text: "",
|
||||||
link: "text-primary underline-offset-4 hover:underline"
|
link: "text-primary underline-offset-4 hover:underline"
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 rounded-md px-3",
|
||||||
sm: "h-8 rounded-md px-3",
|
sm: "h-8 rounded-md px-3",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-9 w-9"
|
icon: "h-9 w-9 rounded-md"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
@ -1,155 +1,183 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
|
||||||
import { Command as CommandPrimitive } from "cmdk";
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Search } from "lucide-react";
|
import { SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
function Command({
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
<CommandPrimitive
|
return (
|
||||||
ref={ref}
|
<CommandPrimitive
|
||||||
className={cn(
|
data-slot="command"
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
className={cn(
|
||||||
className
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
));
|
/>
|
||||||
Command.displayName = CommandPrimitive.displayName;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
className?: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
function CommandInput({
|
||||||
return (
|
className,
|
||||||
<Dialog {...props}>
|
...props
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
return (
|
||||||
{children}
|
<div
|
||||||
</Command>
|
data-slot="command-input-wrapper"
|
||||||
</DialogContent>
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
</Dialog>
|
>
|
||||||
);
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
};
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
function CommandList({
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
return (
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<CommandPrimitive.List
|
||||||
<CommandPrimitive.Input
|
data-slot="command-list"
|
||||||
ref={ref}
|
className={cn(
|
||||||
className={cn(
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
className
|
||||||
className
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
</div>
|
}
|
||||||
));
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
function CommandGroup({
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
<CommandPrimitive.List
|
return (
|
||||||
ref={ref}
|
<CommandPrimitive.Group
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
data-slot="command-group"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
));
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
function CommandItem({
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
...props
|
||||||
>((props, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
<CommandPrimitive.Empty
|
return (
|
||||||
ref={ref}
|
<CommandPrimitive.Item
|
||||||
className="py-6 text-center text-sm"
|
data-slot="command-item"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
));
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
const CommandGroup = React.forwardRef<
|
...props
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
}: React.ComponentProps<"span">) {
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
return (
|
||||||
>(({ className, ...props }, ref) => (
|
<span
|
||||||
<CommandPrimitive.Group
|
data-slot="command-shortcut"
|
||||||
ref={ref}
|
className={cn(
|
||||||
className={cn(
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
className
|
||||||
className
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
));
|
}
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none rounded-md items-center px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
CommandShortcut.displayName = "CommandShortcut";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,8 +93,8 @@ export function DataTable<TData, TValue>({
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
|
||||||
<div className="flex items-center max-w-sm w-full relative mr-2">
|
<div className="flex items-center w-full sm:max-w-sm sm:mr-2 relative">
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={globalFilter ?? ""}
|
value={globalFilter ?? ""}
|
||||||
|
@ -105,7 +105,7 @@ export function DataTable<TData, TValue>({
|
||||||
/>
|
/>
|
||||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 sm:justify-end">
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-99 data-[state=open]:zoom-in-99 data-[state=closed]:slide-out-to-bottom-[5%] data-[state=open]:slide-in-from-bottom-[5%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -2,199 +2,263 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal
|
||||||
|
data-slot="dropdown-menu-portal"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group
|
||||||
|
data-slot="dropdown-menu-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
function DropdownMenuCheckboxItem({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
children,
|
||||||
inset?: boolean
|
checked,
|
||||||
}
|
...props
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
return (
|
||||||
ref={ref}
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
className={cn(
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
className={cn(
|
||||||
inset && "pl-8",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
checked={checked}
|
||||||
>
|
{...props}
|
||||||
{children}
|
>
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
));
|
<CheckIcon className="size-4" />
|
||||||
DropdownMenuSubTrigger.displayName =
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
function DropdownMenuRadioGroup({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
>(({ className, ...props }, ref) => (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
ref={ref}
|
data-slot="dropdown-menu-radio-group"
|
||||||
className={cn(
|
{...props}
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
/>
|
||||||
className
|
);
|
||||||
)}
|
}
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
function DropdownMenuRadioItem({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
children,
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
...props
|
||||||
<DropdownMenuPrimitive.Portal>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
<DropdownMenuPrimitive.Content
|
return (
|
||||||
ref={ref}
|
<DropdownMenuPrimitive.RadioItem
|
||||||
sideOffset={sideOffset}
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
</DropdownMenuPrimitive.Portal>
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
));
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
function DropdownMenuLabel({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
inset,
|
||||||
inset?: boolean
|
...props
|
||||||
}
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
>(({ className, inset, ...props }, ref) => (
|
inset?: boolean;
|
||||||
<DropdownMenuPrimitive.Item
|
}) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<DropdownMenuPrimitive.Label
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
data-slot="dropdown-menu-label"
|
||||||
inset && "pl-8",
|
data-inset={inset}
|
||||||
className
|
className={cn(
|
||||||
)}
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
));
|
{...props}
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
function DropdownMenuSeparator({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
...props
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
return (
|
||||||
ref={ref}
|
<DropdownMenuPrimitive.Separator
|
||||||
className={cn(
|
data-slot="dropdown-menu-separator"
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
checked={checked}
|
);
|
||||||
{...props}
|
}
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
function DropdownMenuShortcut({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
...props
|
||||||
>(({ className, children, ...props }, ref) => (
|
}: React.ComponentProps<"span">) {
|
||||||
<DropdownMenuPrimitive.RadioItem
|
return (
|
||||||
ref={ref}
|
<span
|
||||||
className={cn(
|
data-slot="dropdown-menu-shortcut"
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
className={cn(
|
||||||
className
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
/>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
);
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
}
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
function DropdownMenuSub({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
inset?: boolean
|
return (
|
||||||
}
|
<DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
>(({ className, inset, ...props }, ref) => (
|
);
|
||||||
<DropdownMenuPrimitive.Label
|
}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
function DropdownMenuSubTrigger({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
inset,
|
||||||
>(({ className, ...props }, ref) => (
|
children,
|
||||||
<DropdownMenuPrimitive.Separator
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
inset?: boolean;
|
||||||
{...props}
|
}) {
|
||||||
/>
|
return (
|
||||||
));
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
function DropdownMenuSubContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<DropdownMenuPrimitive.SubContent
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
data-slot="dropdown-menu-sub-content"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
);
|
className
|
||||||
};
|
)}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuPortal,
|
||||||
DropdownMenuContent,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuItem,
|
DropdownMenuContent,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuLabel,
|
||||||
DropdownMenuLabel,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuGroup,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuPortal,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuSub,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuSubContent
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,15 +5,16 @@ import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
|
||||||
FieldPath,
|
|
||||||
FieldValues,
|
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
@ -44,8 +45,8 @@ const FormField = <
|
||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext);
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
const itemContext = React.useContext(FormItemContext);
|
const itemContext = React.useContext(FormItemContext);
|
||||||
const { getFieldState, formState } = useFormContext();
|
const { getFieldState } = useFormContext();
|
||||||
|
const formState = useFormState({ name: fieldContext.name });
|
||||||
const fieldState = getFieldState(fieldContext.name, formState);
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
|
@ -72,47 +73,44 @@ const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue
|
{} as FormItemContextValue
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const id = React.useId();
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
FormItem.displayName = "FormItem";
|
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
function FormLabel({
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const { error, formItemId } = useFormField();
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
data-slot="form-label"
|
||||||
className={cn(error && "text-destructive", className)}
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
FormLabel.displayName = "FormLabel";
|
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
React.ElementRef<typeof Slot>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
|
||||||
>(({ ...props }, ref) => {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
useFormField();
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
data-slot="form-control"
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
|
@ -123,32 +121,24 @@ const FormControl = React.forwardRef<
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
FormControl.displayName = "FormControl";
|
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { formDescriptionId } = useFormField();
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
data-slot="form-description"
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
FormDescription.displayName = "FormDescription";
|
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, children, ...props }, ref) => {
|
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children;
|
const body = error ? String(error?.message ?? "") : props.children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -156,16 +146,15 @@ const FormMessage = React.forwardRef<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
data-slot="form-message"
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn("text-sm font-medium text-destructive", className)}
|
className={cn("text-destructive text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
FormMessage.displayName = "FormMessage";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
|
|
|
@ -2,70 +2,80 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { OTPInput, OTPInputContext } from "input-otp";
|
import { OTPInput, OTPInputContext } from "input-otp";
|
||||||
import { Dot } from "lucide-react";
|
import { MinusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const InputOTP = React.forwardRef<
|
function InputOTP({
|
||||||
React.ElementRef<typeof OTPInput>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof OTPInput> & { obscured?: boolean }
|
containerClassName,
|
||||||
>(({ className, containerClassName, obscured = false, ...props }, ref) => (
|
obscured = false,
|
||||||
<OTPInput
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
containerClassName={cn(
|
containerClassName?: string;
|
||||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
obscured?: boolean;
|
||||||
containerClassName
|
}) {
|
||||||
)}
|
return (
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
<OTPInput
|
||||||
{...props}
|
data-slot="input-otp"
|
||||||
/>
|
containerClassName={cn(
|
||||||
));
|
"flex items-center gap-2 has-disabled:opacity-50",
|
||||||
InputOTP.displayName = "InputOTP";
|
containerClassName
|
||||||
|
)}
|
||||||
|
className={cn("disabled:cursor-not-allowed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const InputOTPGroup = React.forwardRef<
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
React.ElementRef<"div">,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"div">
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="input-otp-group"
|
||||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
className={cn("flex items-center", className)}
|
||||||
));
|
{...props}
|
||||||
InputOTPGroup.displayName = "InputOTPGroup";
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const InputOTPSlot = React.forwardRef<
|
function InputOTPSlot({
|
||||||
React.ElementRef<"div">,
|
index,
|
||||||
React.ComponentPropsWithoutRef<"div"> & { index: number; obscured?: boolean }
|
className,
|
||||||
>(({ index, className, obscured = false, ...props }, ref) => {
|
obscured = false,
|
||||||
const inputOTPContext = React.useContext(OTPInputContext);
|
...props
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
}: React.ComponentProps<"div"> & {
|
||||||
|
index: number;
|
||||||
|
obscured?: boolean;
|
||||||
|
}) {
|
||||||
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
|
const { char, hasFakeCaret, isActive } =
|
||||||
|
inputOTPContext?.slots[index] ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
data-slot="input-otp-slot"
|
||||||
className={cn(
|
data-active={isActive}
|
||||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
className={cn(
|
||||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{char && obscured ? "•" : char}
|
{char && obscured ? "•" : char}
|
||||||
{hasFakeCaret && (
|
{hasFakeCaret && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
});
|
|
||||||
InputOTPSlot.displayName = "InputOTPSlot";
|
|
||||||
|
|
||||||
const InputOTPSeparator = React.forwardRef<
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
React.ElementRef<"div">,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"div">
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
>(({ ...props }, ref) => (
|
<MinusIcon />
|
||||||
<div ref={ref} role="separator" {...props}>
|
</div>
|
||||||
<Dot />
|
);
|
||||||
</div>
|
}
|
||||||
));
|
|
||||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
|
|
|
@ -14,8 +14,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -38,8 +41,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -2,25 +2,22 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const labelVariants = cva(
|
function Label({
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className,
|
||||||
);
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const Label = React.forwardRef<
|
return (
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
<LabelPrimitive.Root
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
data-slot="label"
|
||||||
VariantProps<typeof labelVariants>
|
className={cn(
|
||||||
>(({ className, ...props }, ref) => (
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
<LabelPrimitive.Root
|
className
|
||||||
ref={ref}
|
)}
|
||||||
className={cn(labelVariants(), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
));
|
}
|
||||||
Label.displayName = LabelPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Label };
|
export { Label };
|
||||||
|
|
|
@ -2,39 +2,46 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root;
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const PopoverTrigger = React.forwardRef<
|
function PopoverTrigger({
|
||||||
React.ElementRef<typeof PopoverPrimitive.Trigger>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
>(({ className, ...props }, ref) => (
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
<PopoverPrimitive.Trigger
|
}
|
||||||
ref={ref}
|
|
||||||
className={cn(className, "rounded-md")}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
function PopoverContent({
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
align = "center",
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
sideOffset = 4,
|
||||||
<PopoverPrimitive.Portal>
|
...props
|
||||||
<PopoverPrimitive.Content
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
ref={ref}
|
return (
|
||||||
align={align}
|
<PopoverPrimitive.Portal>
|
||||||
sideOffset={sideOffset}
|
<PopoverPrimitive.Content
|
||||||
className={cn(
|
data-slot="popover-content"
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
align={align}
|
||||||
className
|
sideOffset={sideOffset}
|
||||||
)}
|
className={cn(
|
||||||
{...props}
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
/>
|
className
|
||||||
</PopoverPrimitive.Portal>
|
)}
|
||||||
));
|
{...props}
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent };
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|
|
@ -2,160 +2,189 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root;
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group;
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value;
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
function SelectTrigger({
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
size = "default",
|
||||||
>(({ className, children, ...props }, ref) => (
|
children,
|
||||||
<SelectPrimitive.Trigger
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
className={cn(
|
size?: "sm" | "default";
|
||||||
"flex h-9 w-full items-center justify-between border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
}) {
|
||||||
"rounded-md",
|
return (
|
||||||
className
|
<SelectPrimitive.Trigger
|
||||||
)}
|
data-slot="select-trigger"
|
||||||
{...props}
|
data-size={size}
|
||||||
>
|
className={cn(
|
||||||
{children}
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
|
||||||
<SelectPrimitive.Icon asChild>
|
className
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
)}
|
||||||
</SelectPrimitive.Icon>
|
{...props}
|
||||||
</SelectPrimitive.Trigger>
|
>
|
||||||
));
|
{children}
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
function SelectContent({
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
children,
|
||||||
>(({ className, ...props }, ref) => (
|
position = "popper",
|
||||||
<SelectPrimitive.ScrollUpButton
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
className={cn(
|
return (
|
||||||
"flex cursor-default items-center justify-center py-1",
|
<SelectPrimitive.Portal>
|
||||||
className
|
<SelectPrimitive.Content
|
||||||
)}
|
data-slot="select-content"
|
||||||
{...props}
|
className={cn(
|
||||||
>
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
<ChevronUp className="h-4 w-4" />
|
position === "popper" &&
|
||||||
</SelectPrimitive.ScrollUpButton>
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
));
|
className
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
function SelectLabel({
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
<SelectPrimitive.ScrollDownButton
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Label
|
||||||
className={cn(
|
data-slot="select-label"
|
||||||
"flex cursor-default items-center justify-center py-1",
|
className={cn(
|
||||||
className
|
"text-muted-foreground px-2 py-1.5 text-xs",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<ChevronDown className="h-4 w-4" />
|
/>
|
||||||
</SelectPrimitive.ScrollDownButton>
|
);
|
||||||
));
|
}
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName;
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
function SelectItem({
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
children,
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
...props
|
||||||
<SelectPrimitive.Portal>
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
<SelectPrimitive.Content
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Item
|
||||||
className={cn(
|
data-slot="select-item"
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
className={cn(
|
||||||
position === "popper" &&
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
className
|
||||||
className
|
)}
|
||||||
)}
|
{...props}
|
||||||
position={position}
|
>
|
||||||
{...props}
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<SelectScrollUpButton />
|
<CheckIcon className="size-4" />
|
||||||
<SelectPrimitive.Viewport
|
</SelectPrimitive.ItemIndicator>
|
||||||
className={cn(
|
</span>
|
||||||
"p-1",
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
position === "popper" &&
|
</SelectPrimitive.Item>
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
);
|
||||||
)}
|
}
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
));
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
function SelectSeparator({
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
<SelectPrimitive.Label
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Separator
|
||||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
data-slot="select-separator"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-border pointer-events-none -mx-1 my-1 h-px",
|
||||||
));
|
className
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
function SelectScrollUpButton({
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
...props
|
||||||
>(({ className, children, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
<SelectPrimitive.Item
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.ScrollUpButton
|
||||||
className={cn(
|
data-slot="select-scroll-up-button"
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
className={cn(
|
||||||
className
|
"flex cursor-default items-center justify-center py-1",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<ChevronUpIcon className="size-4" />
|
||||||
<Check className="h-4 w-4" />
|
</SelectPrimitive.ScrollUpButton>
|
||||||
</SelectPrimitive.ItemIndicator>
|
);
|
||||||
</span>
|
}
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
function SelectScrollDownButton({
|
||||||
</SelectPrimitive.Item>
|
className,
|
||||||
));
|
...props
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
const SelectSeparator = React.forwardRef<
|
<SelectPrimitive.ScrollDownButton
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
data-slot="select-scroll-down-button"
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
className={cn(
|
||||||
>(({ className, ...props }, ref) => (
|
"flex cursor-default items-center justify-center py-1",
|
||||||
<SelectPrimitive.Separator
|
className
|
||||||
ref={ref}
|
)}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
/>
|
<ChevronDownIcon className="size-4" />
|
||||||
));
|
</SelectPrimitive.ScrollDownButton>
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
SelectContent,
|
||||||
SelectValue,
|
SelectGroup,
|
||||||
SelectTrigger,
|
SelectItem,
|
||||||
SelectContent,
|
SelectLabel,
|
||||||
SelectLabel,
|
SelectScrollDownButton,
|
||||||
SelectItem,
|
SelectScrollUpButton,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectTrigger,
|
||||||
SelectScrollDownButton,
|
SelectValue
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
function Switch({
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
<SwitchPrimitives.Root
|
return (
|
||||||
className={cn(
|
<SwitchPrimitive.Root
|
||||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
data-slot="switch"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"cursor-pointer peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
{...props}
|
className
|
||||||
ref={ref}
|
)}
|
||||||
>
|
{...props}
|
||||||
<SwitchPrimitives.Thumb
|
>
|
||||||
className={cn(
|
<SwitchPrimitive.Thumb
|
||||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5"
|
data-slot="switch-thumb"
|
||||||
)}
|
className={cn(
|
||||||
/>
|
"cursor-pointer bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-3.5 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(112%-2px)] data-[state=unchecked]:translate-x-[calc(18%)]"
|
||||||
</SwitchPrimitives.Root>
|
)}
|
||||||
));
|
/>
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export { Switch };
|
export { Switch };
|
||||||
|
|
|
@ -10,120 +10,120 @@ import { cn } from "@app/lib/cn";
|
||||||
const ToastProvider = ToastPrimitives.Provider;
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
const ToastViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 md:max-w-[420px]",
|
"fixed bottom-0 left-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 md:max-w-[420px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
|
"shadow-sm group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border bg-card text-foreground",
|
default: "border bg-card text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
"destructive group border-destructive bg-destructive text-destructive-foreground"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
VariantProps<typeof toastVariants>
|
VariantProps<typeof toastVariants>
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Action
|
<ToastPrimitives.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Close
|
<ToastPrimitives.Close
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
));
|
));
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Title
|
<ToastPrimitives.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm font-semibold", className)}
|
className={cn("text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Description
|
<ToastPrimitives.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm opacity-90", className)}
|
className={cn("text-sm opacity-90", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
type ToastActionElement,
|
type ToastActionElement,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction,
|
ToastAction
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,34 +2,42 @@
|
||||||
|
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport
|
||||||
} from "@/components/ui/toast";
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast();
|
const { toasts } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
{toasts.map(function ({
|
||||||
return (
|
id,
|
||||||
<Toast key={id} {...props} className="mt-2">
|
title,
|
||||||
<div className="grid gap-1">
|
description,
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
action,
|
||||||
{description && (
|
...props
|
||||||
<ToastDescription>{description}</ToastDescription>
|
}) {
|
||||||
)}
|
return (
|
||||||
</div>
|
<Toast key={id} {...props} className="mt-2">
|
||||||
{action}
|
<div className="grid gap-1">
|
||||||
<ToastClose />
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
</Toast>
|
{description && (
|
||||||
);
|
<ToastDescription>
|
||||||
})}
|
{description}
|
||||||
<ToastViewport />
|
</ToastDescription>
|
||||||
</ToastProvider>
|
)}
|
||||||
);
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
60
src/components/ui/tooltip.tsx
Normal file
60
src/components/ui/tooltip.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
Loading…
Add table
Add a link
Reference in a new issue