Skip to content

feat: add new nested routes #3027

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0306efc
fix: nested route stlye
ogzhanolguncu Mar 24, 2025
0a63a80
chore: run fmt
ogzhanolguncu Mar 24, 2025
688cdb5
feat: organize and improve sidebar
ogzhanolguncu Mar 24, 2025
269987b
feat: add proper nesting
ogzhanolguncu Mar 24, 2025
b3e10eb
fix: loading issue
ogzhanolguncu Mar 24, 2025
e217396
fix: truncate issue
ogzhanolguncu Mar 24, 2025
0f638c8
feat: add static routes
ogzhanolguncu Mar 24, 2025
0889226
feat: add ratelimit to navbar
ogzhanolguncu Mar 24, 2025
ad4f0f4
fix: small ui issues
ogzhanolguncu Mar 24, 2025
c6a0ddf
Merge branch 'main' of github.com:unkeyed/unkey into add-new-nested-r…
ogzhanolguncu Mar 24, 2025
c72b399
chore: remove comments
ogzhanolguncu Mar 24, 2025
ae91abd
chore: fmt
ogzhanolguncu Mar 24, 2025
7edc851
fix: ui improvements
ogzhanolguncu Mar 25, 2025
ad31ef1
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 25, 2025
da88337
fix: show nested routes on parent click
ogzhanolguncu Mar 25, 2025
0f9c56b
fix: remove unused db query
ogzhanolguncu Mar 27, 2025
31903d7
Merge branch 'main' of https://github.com/unkeyed/unkey into add-new-…
chronark Mar 31, 2025
5371d13
Merge branch 'main' into add-new-nested-routes
chronark Apr 1, 2025
2d7239f
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 1, 2025
4e45de9
Merge branch 'main' into add-new-nested-routes
ogzhanolguncu Apr 7, 2025
cdf5b64
Merge branch 'main' into add-new-nested-routes
ogzhanolguncu Apr 7, 2025
5dc0af9
fix: type issue
ogzhanolguncu Apr 7, 2025
efabd52
fix: revalidation issue
ogzhanolguncu Apr 7, 2025
c6da336
fix: ui issues
ogzhanolguncu Apr 7, 2025
59d5c5f
fix: show loader only if that item has Icon
ogzhanolguncu Apr 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/dashboard/app/(app)/apis/_components/api-list-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Props = {
};

export const ApiListCard = ({ api }: Props) => {
const { timeseries, isLoading, isError } = useFetchVerificationTimeseries(api.keyspaceId);
const { timeseries, isError } = useFetchVerificationTimeseries(api.keyspaceId);

const passed = timeseries?.reduce((acc, crr) => acc + crr.success, 0) ?? 0;
const blocked = timeseries?.reduce((acc, crr) => acc + crr.error, 0) ?? 0;
Expand All @@ -26,7 +26,8 @@ export const ApiListCard = ({ api }: Props) => {
chart={
<StatsTimeseriesBarChart
data={timeseries}
isLoading={isLoading}
// INFO: Causing too much lag when there are too many Charts. We'll try to optimize this in the future.
isLoading={false}
isError={isError}
config={{
success: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ApiListGrid = ({
<ApiListCard api={api} key={api.id} />
))}
</div>
<div className="flex flex-col items-center justify-center mt-8 space-y-4">
<div className="flex flex-col items-center justify-center mt-8 space-y-4 pb-8">
<div className="text-center text-sm text-accent-11">
Showing {apiList.length} of {total} APIs
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const CreateApiButton = ({
}: React.ButtonHTMLAttributes<HTMLButtonElement> & Props) => {
const [isOpen, setIsOpen] = useState(defaultOpen ?? false);
const router = useRouter();
const { api } = trpc.useUtils();

const {
register,
Expand All @@ -50,6 +51,7 @@ export const CreateApiButton = ({
async onSuccess(res) {
toast.success("Your API has been created");
await revalidate("/apis");
api.overview.query.invalidate();
router.push(`/apis/${res.id}`);
setIsOpen(false);
},
Expand Down
20 changes: 19 additions & 1 deletion apps/dashboard/app/(app)/apis/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export async function fetchApiOverview({
.where(and(eq(schema.apis.workspaceId, workspaceId), isNull(schema.apis.deletedAtM)));
const total = Number(totalResult[0]?.count || 0);

// Updated query to include keyAuth and fetch actual keys
const query = db.query.apis.findMany({
where: (table, { and, eq, isNull, gt }) => {
const conditions = [eq(table.workspaceId, workspaceId), isNull(table.deletedAtM)];
Expand All @@ -30,6 +31,7 @@ export async function fetchApiOverview({
with: {
keyAuth: {
columns: {
id: true, // Include the keyspace ID
sizeApprox: true,
},
},
Expand All @@ -44,7 +46,23 @@ export async function fetchApiOverview({
const nextCursor =
hasMore && apiItems.length > 0 ? { id: apiItems[apiItems.length - 1].id } : undefined;

const apiList = await apiItemsWithApproxKeyCounts(apiItems);
// Transform the data to include key information
const apiList = await Promise.all(
apiItems.map(async (api) => {
const keyspaceId = api.keyAuth?.id || null;

return {
id: api.id,
name: api.name,
keyspaceId,
keys: [
{
count: api.keyAuth?.sizeApprox || 0,
},
],
};
}),
);

return {
apiList,
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppSidebar } from "@/components/app-sidebar";
import { AppSidebar } from "@/components/navigation/sidebar/app-sidebar";
import { SidebarMobile } from "@/components/navigation/sidebar/sidebar-mobile";
import { SidebarProvider } from "@/components/ui/sidebar";
import { getOrgId } from "@/lib/auth";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { revalidate } from "@/app/actions";
import { DialogContainer } from "@/components/dialog-container";
import { NavbarActionButton } from "@/components/navigation/action-button";
import { toast } from "@/components/ui/toaster";
Expand Down Expand Up @@ -32,6 +33,8 @@ export const CreateNamespaceButton = ({
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const [isOpen, setIsOpen] = useState(false);

const { ratelimit } = trpc.useUtils();

const {
register,
handleSubmit,
Expand All @@ -44,9 +47,11 @@ export const CreateNamespaceButton = ({
const router = useRouter();

const create = trpc.ratelimit.namespace.create.useMutation({
onSuccess(res) {
async onSuccess(res) {
toast.success("Your Namespace has been created");
router.refresh();
await revalidate("/ratelimits");
ratelimit.namespace.query.invalidate();
router.push(`/ratelimits/${res.id}`);
setIsOpen(false);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export const Client: React.FC<Props> = ({ apis }) => {
}
}}
>
<DialogContent className="flex flex-col max-sm:w-full bg-grayA-1 border-gray-4">
<DialogContent className="flex flex-col max-sm:w-full dark:bg-grayA-1 border-gray-4 bg-white">
<DialogHeader>
<DialogTitle>Your API Key</DialogTitle>
<DialogDescription className="w-fit text-accent-10">
Expand Down
8 changes: 5 additions & 3 deletions apps/dashboard/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ const FlatNavItem = memo(({ item }: { item: NavItem }) => {
isActive={item.active}
className={getButtonStyles(item.active, showLoader)}
>
{showLoader ? <AnimatedLoadingSpinner /> : <Icon size="xl-medium" />}
{showLoader ? <AnimatedLoadingSpinner /> : Icon ? <Icon size="xl-medium" /> : null}
<span>{item.label}</span>
</SidebarMenuButton>
</NavLink>
Expand All @@ -264,7 +264,7 @@ const NestedNavItem = memo(({ item }: { item: NavItem & { items?: NavItem[] } })
isActive={item.active}
className={getButtonStyles(item.active, showLoader)}
>
{showLoader ? <AnimatedLoadingSpinner /> : <Icon />}
{showLoader ? <AnimatedLoadingSpinner /> : Icon ? <Icon size="xl-medium" /> : null}
<span>{item.label}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
Expand Down Expand Up @@ -336,6 +336,8 @@ const ToggleSidebarButton = memo(
toggleNavItem: NavItem;
toggleSidebar: () => void;
}) => {
const Icon = toggleNavItem.icon;

return (
<SidebarMenuItem>
<SidebarMenuButton
Expand All @@ -344,7 +346,7 @@ const ToggleSidebarButton = memo(
className={getButtonStyles(toggleNavItem.active)}
onClick={toggleSidebar}
>
<toggleNavItem.icon size="xl-medium" />
{Icon && <Icon size="xl-medium" />}
<span>{toggleNavItem.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getPathForSegment } from "./utils";

// Define style ID to check for duplicates
const STYLE_ID = "animated-loading-spinner-styles";

// Add styles only once when module is loaded
if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
@media (prefers-reduced-motion: reduce) {
[data-prefers-reduced-motion="respect-motion-preference"] {
animation: none !important;
transition: none !important;
}
}

@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.animate-spin-slow {
animation: spin-slow 1.5s linear infinite;
}
`;
document.head.appendChild(style);
}

const SEGMENTS = [
"segment-1", // Right top
"segment-2", // Right
"segment-3", // Right bottom
"segment-4", // Bottom
"segment-5", // Left bottom
"segment-6", // Left
"segment-7", // Left top
"segment-8", // Top
];

export const AnimatedLoadingSpinner = () => {
const [segmentIndex, setSegmentIndex] = useState(0);

useEffect(() => {
// Animate the segments in sequence
const timer = setInterval(() => {
setSegmentIndex((prevIndex) => (prevIndex + 1) % SEGMENTS.length);
}, 125); // 125ms per segment = 1s for full rotation

return () => clearInterval(timer);
}, []);

return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
className="animate-spin-slow"
data-prefers-reduced-motion="respect-motion-preference"
>
<g>
{SEGMENTS.map((id, index) => {
const distance = (SEGMENTS.length + index - segmentIndex) % SEGMENTS.length;
const opacity = distance <= 4 ? 1 - distance * 0.2 : 0.1;
return (
<path
key={id}
id={id}
style={{
fill: "currentColor",
opacity: opacity,
transition: "opacity 0.12s ease-in-out",
}}
d={getPathForSegment(index)}
/>
);
})}
<path
d="M9,6.5c-1.379,0-2.5,1.121-2.5,2.5s1.121,2.5,2.5,2.5,2.5-1.121,2.5-2.5-1.121-2.5-2.5-2.5Z"
style={{ fill: "currentColor", opacity: 0.6 }}
/>
</g>
</svg>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import { useDelayLoader } from "@/hooks/use-delay-loader";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import type { NavItem } from "../../../workspace-navigations";
import { NavLink } from "../nav-link";
import { AnimatedLoadingSpinner } from "./animated-loading-spinner";
import { getButtonStyles } from "./utils";

export const FlatNavItem = ({
item,
onLoadMore,
}: {
item: NavItem & { loadMoreAction?: boolean };
onLoadMore?: () => void;
}) => {
const [isPending, startTransition] = useTransition();
const showLoader = useDelayLoader(isPending);
const router = useRouter();
const Icon = item.icon;

const isLoadMoreButton = item.loadMoreAction === true;

const handleClick = () => {
if (isLoadMoreButton && onLoadMore) {
onLoadMore();
return;
}

if (!item.external) {
startTransition(() => {
router.push(item.href);
});
}
};

return (
<SidebarMenuItem>
<NavLink
href={item.href}
external={item.external}
onClick={handleClick}
isLoadMoreButton={isLoadMoreButton}
>
<SidebarMenuButton
tooltip={item.tooltip}
isActive={item.active}
className={getButtonStyles(item.active, showLoader)}
>
{showLoader ? <AnimatedLoadingSpinner /> : Icon ? <Icon size="xl-medium" /> : null}
<span>{item.label}</span>
{item.tag && <div className="ml-auto">{item.tag}</div>}
</SidebarMenuButton>
</NavLink>
</SidebarMenuItem>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NavItem } from "../../../workspace-navigations";
import { FlatNavItem } from "./flat-nav-item";
import { NestedNavItem } from "./nested-nav-item";

export const NavItems = ({
item,
onLoadMore,
}: {
item: NavItem & {
items?: (NavItem & { loadMoreAction?: boolean })[];
loadMoreAction?: boolean;
};
onLoadMore?: () => void;
}) => {
if (!item.items || item.items.length === 0) {
return <FlatNavItem item={item} onLoadMore={onLoadMore} />;
}
return <NestedNavItem item={item} onLoadMore={onLoadMore} />;
};
Loading
Loading