2.3.0 Dev Tools (#180)

* 2.3.0 - Added new Dev Tools app
This commit is contained in:
Giancarlo Buomprisco
2025-02-21 13:29:42 +07:00
committed by GitHub
parent 59dfc0ad91
commit c185bcfa11
36 changed files with 3747 additions and 67 deletions

View File

@@ -0,0 +1,13 @@
import { DevToolSidebar } from '@/components/app-sidebar';
import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar';
export function DevToolLayout(props: React.PropsWithChildren) {
return (
<SidebarProvider>
<DevToolSidebar />
<SidebarInset>{props.children}</SidebarInset>
</SidebarProvider>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
BoltIcon,
LanguagesIcon,
LayoutDashboardIcon,
MailIcon,
} from 'lucide-react';
import {
Sidebar,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@kit/ui/shadcn-sidebar';
import { isRouteActive } from '@kit/ui/utils';
const routes = [
{
label: 'Dashboard',
path: '/',
Icon: LayoutDashboardIcon,
},
{
label: 'Environment Variables',
path: '/variables',
Icon: BoltIcon,
},
{
label: 'Emails',
path: '/emails',
Icon: MailIcon,
},
{
label: 'Translations',
path: '/translations',
Icon: LanguagesIcon,
},
];
export function DevToolSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<b className="p-1 font-mono text-xs font-semibold">Makerkit Dev Tool</b>
</SidebarHeader>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Dev Tools</SidebarGroupLabel>
<SidebarMenu>
{routes.map((route) => (
<SidebarMenuItem key={route.path}>
<SidebarMenuButton
isActive={isRouteActive(route.path, pathname, false)}
asChild
>
<Link href={route.path}>
<route.Icon className="h-4 w-4" />
<span>{route.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</Sidebar>
);
}

View File

@@ -0,0 +1,41 @@
'use client';
import { useRouter } from 'next/navigation';
import { EnvMode } from '@/app/variables/lib/types';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
export function EnvModeSelector({ mode }: { mode: EnvMode }) {
const router = useRouter();
const handleModeChange = (value: EnvMode) => {
const searchParams = new URLSearchParams(window.location.search);
const path = window.location.pathname;
searchParams.set('mode', value);
router.push(`${path}?${searchParams.toString()}`);
};
return (
<div>
<Select name={'mode'} defaultValue={mode} onValueChange={handleModeChange}>
<SelectTrigger>
<SelectValue placeholder="Select Mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="development">Development</SelectItem>
<SelectItem value="production">Production</SelectItem>
</SelectContent>
</Select>
</div>
);
}

View File

@@ -0,0 +1,35 @@
'use client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
export const IFrame: React.FC<
React.IframeHTMLAttributes<unknown> & {
setInnerRef?: (ref: HTMLIFrameElement | undefined) => void;
appendStyles?: boolean;
theme?: 'light' | 'dark';
transparent?: boolean;
}
> = ({ children, setInnerRef, appendStyles = true, theme, ...props }) => {
const [ref, setRef] = useState<HTMLIFrameElement | null>();
const doc = ref?.contentWindow?.document as Document;
const mountNode = doc?.body;
return (
<iframe
{...props}
ref={(ref) => {
if (ref) {
setRef(ref);
if (setInnerRef) {
setInnerRef(ref);
}
}
}}
>
{mountNode ? createPortal(children, mountNode) : null}
</iframe>
);
};

View File

@@ -0,0 +1,32 @@
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@kit/ui/sonner';
export function RootProviders({ children }: React.PropsWithChildren) {
return <ReactQueryProvider>{children}</ReactQueryProvider>;
}
function ReactQueryProvider(props: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>
{props.children}
<Toaster position="top-center" />
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import { AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Card, CardContent } from '@kit/ui/card';
export const ServiceStatus = {
CHECKING: 'checking',
SUCCESS: 'success',
ERROR: 'error',
} as const;
type ServiceStatusType = (typeof ServiceStatus)[keyof typeof ServiceStatus];
const StatusIcons = {
[ServiceStatus.CHECKING]: <AlertCircle className="h-6 w-6 text-yellow-500" />,
[ServiceStatus.SUCCESS]: <CheckCircle2 className="h-6 w-6 text-green-500" />,
[ServiceStatus.ERROR]: <XCircle className="h-6 w-6 text-red-500" />,
};
interface ServiceCardProps {
name: string;
status: {
status: ServiceStatusType;
message?: string;
};
}
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
return (
<Card className="w-full max-w-2xl">
<CardContent className="pt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{StatusIcons[status.status]}
<div>
<h3 className="font-medium">{name}</h3>
<p className="text-sm text-gray-500">
{status.message ??
(status.status === ServiceStatus.CHECKING
? 'Checking connection...'
: status.status === ServiceStatus.SUCCESS
? 'Connected successfully'
: 'Connection failed')}
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
};