mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-16 17:05:04 +02:00
more tweaks to layout
This commit is contained in:
parent
b731a50cc9
commit
8b0c30f19f
22 changed files with 172 additions and 193 deletions
|
@ -45,7 +45,7 @@ export default async function OrgPage(props: OrgPageProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout
|
<Layout
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
navItems={orgNavItems}
|
navItems={orgNavItems}
|
||||||
>
|
>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from "@app/components/ui/breadcrumb";
|
} from "@app/components/ui/breadcrumb";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
|
||||||
interface UserLayoutProps {
|
interface UserLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -48,28 +49,11 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={`${user?.email}`}
|
||||||
|
description="Manage the settings on this user"
|
||||||
|
/>
|
||||||
<OrgUserProvider orgUser={user}>
|
<OrgUserProvider orgUser={user}>
|
||||||
<div className="mb-4">
|
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<Link href="../../">Users</Link>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{user.email}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-0.5 mb-6">
|
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
|
||||||
User {user?.email}
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground">Manage user</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HorizontalTabs items={navItems}>
|
<HorizontalTabs items={navItems}>
|
||||||
{children}
|
{children}
|
||||||
</HorizontalTabs>
|
</HorizontalTabs>
|
||||||
|
|
|
@ -18,6 +18,7 @@ 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";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -26,51 +27,6 @@ export const metadata: Metadata = {
|
||||||
description: ""
|
description: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItems: SidebarNavItem[] = [
|
|
||||||
{
|
|
||||||
title: "Sites",
|
|
||||||
href: "/{orgId}/settings/sites"
|
|
||||||
// icon: <Combine className="h-4 w-4" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Resources",
|
|
||||||
href: "/{orgId}/settings/resources"
|
|
||||||
// icon: <Waypoints className="h-4 w-4" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Access Control",
|
|
||||||
href: "/{orgId}/settings/access",
|
|
||||||
// icon: <Users className="h-4 w-4" />,
|
|
||||||
autoExpand: true,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: "Users",
|
|
||||||
href: "/{orgId}/settings/access/users",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: "Invitations",
|
|
||||||
href: "/{orgId}/settings/access/invitations"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Roles",
|
|
||||||
href: "/{orgId}/settings/access/roles"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Shareable Links",
|
|
||||||
href: "/{orgId}/settings/share-links"
|
|
||||||
// icon: <LinkIcon className="h-4 w-4" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "General",
|
|
||||||
href: "/{orgId}/settings/general"
|
|
||||||
// icon: <Settings className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
interface SettingsLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
@ -119,7 +75,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgId={params.orgId} orgs={orgs} navItems={navItems}>
|
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|
|
@ -569,7 +569,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outlinePrimary"
|
variant="outlinePrimary"
|
||||||
className="mt-8"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Add Target
|
Add Target
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -693,7 +693,7 @@ export default function ResourceRules(props: {
|
||||||
control={addRuleForm.control}
|
control={addRuleForm.control}
|
||||||
name="value"
|
name="value"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="space-y-0 mb-2">
|
||||||
<InfoPopup
|
<InfoPopup
|
||||||
text="Value"
|
text="Value"
|
||||||
info={
|
info={
|
||||||
|
@ -714,6 +714,7 @@ export default function ResourceRules(props: {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outlinePrimary"
|
variant="outlinePrimary"
|
||||||
|
className="mb-2"
|
||||||
disabled={!rulesEnabled}
|
disabled={!rulesEnabled}
|
||||||
>
|
>
|
||||||
Add Rule
|
Add Rule
|
||||||
|
|
|
@ -9,6 +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";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -17,14 +18,6 @@ export const metadata: Metadata = {
|
||||||
description: ""
|
description: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{
|
|
||||||
title: "All Users",
|
|
||||||
href: "/admin/users",
|
|
||||||
icon: <Users className="h-4 w-4" />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +44,7 @@ export default async function AdminLayout(props: LayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgs={orgs} navItems={navItems}>
|
<Layout orgs={orgs} navItems={adminNavItems}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
@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 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
|
@ -1,33 +1,37 @@
|
||||||
import { Home, Settings, Users, Link as LinkIcon, Waypoints, Combine } from "lucide-react";
|
import { SidebarNavItem } from "@app/components/SidebarNav";
|
||||||
|
import {
|
||||||
|
Home,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Link as LinkIcon,
|
||||||
|
Waypoints,
|
||||||
|
Combine
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const rootNavItems = [
|
export const rootNavItems: SidebarNavItem[] = [
|
||||||
{
|
{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
href: "/",
|
href: "/"
|
||||||
icon: <Home className="h-4 w-4" />
|
// icon: <Home className="h-4 w-4" />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const orgNavItems = [
|
export const orgNavItems: SidebarNavItem[] = [
|
||||||
{
|
|
||||||
title: "Overview",
|
|
||||||
href: "/{orgId}",
|
|
||||||
icon: <Home className="h-4 w-4" />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Sites",
|
title: "Sites",
|
||||||
href: "/{orgId}/settings/sites",
|
href: "/{orgId}/settings/sites"
|
||||||
icon: <Combine className="h-4 w-4" />
|
// icon: <Combine className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Resources",
|
title: "Resources",
|
||||||
href: "/{orgId}/settings/resources",
|
href: "/{orgId}/settings/resources"
|
||||||
icon: <Waypoints className="h-4 w-4" />
|
// icon: <Waypoints className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Access Control",
|
title: "Access Control",
|
||||||
href: "/{orgId}/settings/access",
|
href: "/{orgId}/settings/access",
|
||||||
icon: <Users className="h-4 w-4" />,
|
// icon: <Users className="h-4 w-4" />,
|
||||||
|
autoExpand: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "Users",
|
title: "Users",
|
||||||
|
@ -41,12 +45,20 @@ export const orgNavItems = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Shareable Links",
|
title: "Shareable Links",
|
||||||
href: "/{orgId}/settings/share-links",
|
href: "/{orgId}/settings/share-links"
|
||||||
icon: <LinkIcon className="h-4 w-4" />
|
// icon: <LinkIcon className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
href: "/{orgId}/settings/general",
|
href: "/{orgId}/settings/general"
|
||||||
icon: <Settings className="h-4 w-4" />
|
// icon: <Settings className="h-4 w-4" />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const adminNavItems: SidebarNavItem[] = [
|
||||||
|
{
|
||||||
|
title: "All Users",
|
||||||
|
href: "/admin/users"
|
||||||
|
// icon: <Users className="h-4 w-4" />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default async function Page(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout
|
<Layout
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
navItems={rootNavItems}
|
navItems={rootNavItems}
|
||||||
showBreadcrumbs={false}
|
showBreadcrumbs={false}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Layout } from "@app/components/Layout";
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
@ -5,6 +6,7 @@ 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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `Setup - Pangolin`,
|
title: `Setup - Pangolin`,
|
||||||
|
@ -27,27 +29,19 @@ export default async function SetupLayout({
|
||||||
redirect("/?redirect=/setup");
|
redirect("/?redirect=/setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!(!env.flags.disableUserCreateOrg || user.serverAdmin)) {
|
||||||
!(!env.flags.disableUserCreateOrg || user.serverAdmin)
|
|
||||||
) {
|
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-3">
|
<UserProvider user={user}>
|
||||||
{user && (
|
<Layout navItems={rootNavItems} showBreadcrumbs={false}>
|
||||||
<UserProvider user={user}>
|
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
|
||||||
<div>
|
{children}
|
||||||
<ProfileIcon />
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function Breadcrumbs() {
|
||||||
} else if (segment === "resources") {
|
} else if (segment === "resources") {
|
||||||
label = "Resources";
|
label = "Resources";
|
||||||
} else if (segment === "access") {
|
} else if (segment === "access") {
|
||||||
label = "Users & Roles";
|
label = "Access Control";
|
||||||
} else if (segment === "general") {
|
} else if (segment === "general") {
|
||||||
label = "General";
|
label = "General";
|
||||||
} else if (segment === "share-links") {
|
} else if (segment === "share-links") {
|
||||||
|
@ -48,14 +48,14 @@ export function Breadcrumbs() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b px-4 py-2 overflow-x-auto scrollbar-hide bg-card">
|
<div className="border-b px-4 py-2 overflow-x-auto scrollbar-hide bg-card">
|
||||||
<nav className="flex items-center space-x-1 text-sm text-muted-foreground">
|
<nav className="flex items-center space-x-1 text-sm text-muted-foreground whitespace-nowrap">
|
||||||
{breadcrumbs.map((crumb, index) => (
|
{breadcrumbs.map((crumb, index) => (
|
||||||
<div key={crumb.href} className="flex items-center">
|
<div key={crumb.href} className="flex items-center whitespace-nowrap">
|
||||||
{index !== 0 && <ChevronRight className="h-4 w-4" />}
|
{index !== 0 && <ChevronRight className="h-4 w-4" />}
|
||||||
<Link
|
<Link
|
||||||
href={crumb.href}
|
href={crumb.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-1 hover:text-foreground",
|
"ml-1 hover:text-foreground whitespace-nowrap",
|
||||||
index === breadcrumbs.length - 1 &&
|
index === breadcrumbs.length - 1 &&
|
||||||
"text-foreground font-medium"
|
"text-foreground font-medium"
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
|
||||||
width={34}
|
width={34}
|
||||||
height={34}
|
height={34}
|
||||||
/>
|
/>
|
||||||
<span className="font-bold text-2xl">Pangolin</span>
|
<span className="font-[Space_Grotesk] font-bold text-2xl text-neutral-500">Pangolin</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,7 +28,8 @@ export function HorizontalTabs({
|
||||||
return href
|
return href
|
||||||
.replace("{orgId}", params.orgId as string)
|
.replace("{orgId}", params.orgId as string)
|
||||||
.replace("{resourceId}", params.resourceId as string)
|
.replace("{resourceId}", params.resourceId as string)
|
||||||
.replace("{niceId}", params.niceId as string);
|
.replace("{niceId}", params.niceId as string)
|
||||||
|
.replace("{userId}", params.userId as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { ListOrgsResponse } from "@server/routers/org";
|
||||||
import SupporterStatus from "@app/components/SupporterStatus";
|
import SupporterStatus from "@app/components/SupporterStatus";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
import { Separator } from "@app/components/ui/separator";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Menu, X } from "lucide-react";
|
import { ExternalLink, Menu, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
||||||
} from "@app/components/ui/sheet";
|
} from "@app/components/ui/sheet";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Breadcrumbs } from "@app/components/Breadcrumbs";
|
import { Breadcrumbs } from "@app/components/Breadcrumbs";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -68,7 +69,10 @@ export function Layout({
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-64 p-0 flex flex-col h-full">
|
<SheetContent
|
||||||
|
side="left"
|
||||||
|
className="w-64 p-0 flex flex-col h-full"
|
||||||
|
>
|
||||||
<SheetTitle className="sr-only">
|
<SheetTitle className="sr-only">
|
||||||
Navigation Menu
|
Navigation Menu
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
|
@ -81,7 +85,7 @@ export function Layout({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<SidebarNav items={navItems} />
|
<SidebarNav items={navItems} onItemClick={() => setIsMobileMenuOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 space-y-4 border-t shrink-0">
|
<div className="p-4 space-y-4 border-t shrink-0">
|
||||||
<SupporterStatus />
|
<SupporterStatus />
|
||||||
|
@ -113,7 +117,15 @@ export function Layout({
|
||||||
<OrgSelector orgId={orgId} orgs={orgs} />
|
<OrgSelector orgId={orgId} orgs={orgs} />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
Open Source
|
<Link
|
||||||
|
href="https://github.com/fosrl/pangolin"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
Open Source
|
||||||
|
<ExternalLink size={12}/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{env?.app?.version && (
|
{env?.app?.version && (
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
@ -126,7 +138,12 @@ export function Layout({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className={cn("flex-1 flex flex-col h-full min-w-0", !showSidebar && "w-full")}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 flex flex-col h-full min-w-0",
|
||||||
|
!showSidebar && "w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{showTopBar && (
|
{showTopBar && (
|
||||||
<div className="h-16 border-b shrink-0 bg-card">
|
<div className="h-16 border-b shrink-0 bg-card">
|
||||||
<div className="flex h-full items-center justify-end px-4">
|
<div className="flex h-full items-center justify-end px-4">
|
||||||
|
@ -135,7 +152,7 @@ export function Layout({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showBreadcrumbs && <Breadcrumbs />}
|
{showBreadcrumbs && <Breadcrumbs />}
|
||||||
<main className="flex-1 overflow-y-auto p-6 w-full">
|
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,12 +17,14 @@ export interface SidebarNavItem {
|
||||||
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
items: SidebarNavItem[];
|
items: SidebarNavItem[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onItemClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarNav({
|
export function SidebarNav({
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
onItemClick,
|
||||||
...props
|
...props
|
||||||
}: SidebarNavProps) {
|
}: SidebarNavProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
@ -87,13 +89,19 @@ export function SidebarNav({
|
||||||
<Link
|
<Link
|
||||||
href={hydratedHref}
|
href={hydratedHref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center py-2 px-3 w-full transition-colors",
|
"flex items-center py-1 w-full transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "text-primary font-medium"
|
? "text-primary font-medium"
|
||||||
: "text-muted-foreground hover:text-foreground",
|
: "text-muted-foreground hover:text-foreground",
|
||||||
disabled && "cursor-not-allowed opacity-60"
|
disabled && "cursor-not-allowed opacity-60"
|
||||||
)}
|
)}
|
||||||
onClick={disabled ? (e) => e.preventDefault() : undefined}
|
onClick={(e) => {
|
||||||
|
if (disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (onItemClick) {
|
||||||
|
onItemClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
tabIndex={disabled ? -1 : undefined}
|
tabIndex={disabled ? -1 : undefined}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function TopBar({ orgId, orgs }: TopBarProps) {
|
||||||
Documentation
|
Documentation
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="https://fossorial.io/support"
|
href="mailto:support@fossorial.io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
|
|
@ -221,7 +221,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
||||||
modal={usePortal}
|
modal={usePortal}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative h-full flex items-center rounded-md border bg-transparent pr-3"
|
className="relative h-full flex items-center rounded-md border border-input bg-transparent pr-3"
|
||||||
ref={triggerContainerRef}
|
ref={triggerContainerRef}
|
||||||
>
|
>
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { cn } from "@app/lib/cn";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"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-2 disabled:pointer-events-none disabled:opacity-50",
|
"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-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
@ -24,7 +24,12 @@ import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||||
import { Plus, Search } from "lucide-react";
|
import { Plus, Search } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@app/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from "@app/components/ui/card";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
@ -47,6 +52,7 @@ export function DataTable<TData, TValue>({
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [globalFilter, setGlobalFilter] = useState<any>([]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
@ -57,6 +63,7 @@ export function DataTable<TData, TValue>({
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
initialState: {
|
initialState: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
@ -65,7 +72,8 @@ export function DataTable<TData, TValue>({
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
sorting,
|
sorting,
|
||||||
columnFilters
|
columnFilters,
|
||||||
|
globalFilter
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,12 +81,12 @@ export function DataTable<TData, TValue>({
|
||||||
<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-row items-center justify-between space-y-0 pb-4">
|
||||||
<div className="flex items-center max-w-sm w-full relative">
|
<div className="flex items-center max-w-sm w-full relative mr-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={(table.getColumn(searchColumn)?.getFilterValue() as string) ?? ""}
|
value={globalFilter ?? ""}
|
||||||
onChange={(event) =>
|
onChange={(e) =>
|
||||||
table.getColumn(searchColumn)?.setFilterValue(event.target.value)
|
table.setGlobalFilter(String(e.target.value))
|
||||||
}
|
}
|
||||||
className="w-full pl-8"
|
className="w-full pl-8"
|
||||||
/>
|
/>
|
||||||
|
@ -101,7 +109,8 @@ export function DataTable<TData, TValue>({
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef
|
||||||
|
.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
@ -114,7 +123,9 @@ export function DataTable<TData, TValue>({
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={
|
||||||
|
row.getIsSelected() && "selected"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
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 { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
ControllerProps,
|
||||||
FieldPath,
|
FieldPath,
|
||||||
FieldValues,
|
FieldValues,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
useFormContext
|
||||||
} from "react-hook-form"
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn"
|
import { cn } from "@app/lib/cn";
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
const Form = FormProvider
|
const Form = FormProvider;
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
> = {
|
> = {
|
||||||
name: TName
|
name: TName;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue
|
{} as FormFieldContextValue
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
@ -38,21 +38,21 @@ const FormField = <
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
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, formState } = useFormContext();
|
||||||
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
throw new Error("useFormField should be used within <FormField>")
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = itemContext
|
const { id } = itemContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
@ -60,37 +60,37 @@ const useFormField = () => {
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type FormItemContextValue = {
|
type FormItemContextValue = {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue
|
{} as FormItemContextValue
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ 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 ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormItem.displayName = "FormItem"
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
|
@ -99,15 +99,16 @@ const FormLabel = React.forwardRef<
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormLabel.displayName = "FormLabel"
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
|
@ -121,15 +122,15 @@ const FormControl = React.forwardRef<
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormControl.displayName = "FormControl"
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
const FormDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
|
@ -138,19 +139,19 @@ const FormDescription = React.forwardRef<
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormDescription.displayName = "FormDescription"
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
const FormMessage = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ 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) : children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -162,9 +163,9 @@ const FormMessage = React.forwardRef<
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
|
@ -174,5 +175,5 @@ export {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField,
|
FormField
|
||||||
}
|
};
|
||||||
|
|
|
@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 items-center justify-center border-y-2 border-r-2 border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l-2 last:rounded-r-md",
|
"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",
|
||||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"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"
|
"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"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue