Skip to content

Commit a5e3858

Browse files
authored
Merge pull request #2 from elixir-cloud-aai/pages
feat: added service info page, logout and minor fixes
2 parents 3ebbe7c + aeb59e0 commit a5e3858

28 files changed

Lines changed: 1036 additions & 148 deletions

src/app/auth/api/logout/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { deleteCookie } from "cookies-next/server";
2+
import { cookies } from "next/headers";
3+
import { redirect } from "next/navigation";
4+
import { NextRequest } from "next/server";
5+
6+
export async function GET(req: NextRequest) {
7+
try {
8+
await deleteCookie("access_token", { cookies });
9+
await deleteCookie("id_token", { cookies });
10+
} catch (err) {
11+
console.log(err);
12+
redirect("/auth/error?type=logout-err");
13+
}
14+
redirect("/auth/login");
15+
}

src/app/auth/error/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Link from "next/link";
22

33
interface Props {
4-
searchParams: Promise<{ type?: "params" | "state" | "internal" }>;
4+
searchParams: Promise<{ type?: "params" | "state" | "internal" | "logout-err" }>;
55
}
66

77
async function Page({ searchParams }: Props) {
@@ -10,6 +10,8 @@ async function Page({ searchParams }: Props) {
1010

1111
if (type === "params") message = "Invalid request parameters found.";
1212
else if (type === "state") message = "Invalid state found. State does not match.";
13+
else if (type === "logout-err")
14+
message = "An internal server error occurred while logging you out. Please try logging out again.";
1315

1416
return (
1517
<div className="flex w-full h-screen justify-center items-center">

src/app/dashboard/layout.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import { AppSidebar, BreadcrumbClient } from "@/components/shared/app-sidebar";
2-
import {
3-
Breadcrumb,
4-
BreadcrumbItem,
5-
BreadcrumbLink,
6-
BreadcrumbList,
7-
BreadcrumbPage,
8-
BreadcrumbSeparator,
9-
} from "@/components/ui/breadcrumb";
1+
import { AppBreadcrumb, AppSidebar, AppTopbarActions } from "@/components/shared/app-sidebar";
102
import { Separator } from "@/components/ui/separator";
113
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
124

@@ -21,17 +13,10 @@ export default function Layout({ children }: { children: React.ReactNode }) {
2113
orientation="vertical"
2214
className="mr-2 data-[orientation=vertical]:h-4 mt-6"
2315
/>
24-
<Breadcrumb>
25-
<BreadcrumbList>
26-
<BreadcrumbItem className="hidden md:block">
27-
<BreadcrumbLink href="/dashboard">Cloud Registry</BreadcrumbLink>
28-
</BreadcrumbItem>
29-
<BreadcrumbSeparator className="hidden md:block" />
30-
<BreadcrumbItem>
31-
<BreadcrumbPage>{<BreadcrumbClient />}</BreadcrumbPage>
32-
</BreadcrumbItem>
33-
</BreadcrumbList>
34-
</Breadcrumb>
16+
<div className="flex items-center justify-between w-full">
17+
<AppBreadcrumb />
18+
<AppTopbarActions />
19+
</div>
3520
</header>
3621
<main className="relative px-4 md:px-8 py-4 h-full overflow-y-auto">
3722
{children}

src/app/dashboard/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { redirect } from "next/navigation";
2+
13
function Page() {
2-
return <div></div>;
4+
redirect("/dashboard/services");
35
}
46

57
export default Page;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { CardContent } from "@/components/ui/card";
2+
import { Separator } from "@/components/ui/separator";
3+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
4+
import { Service } from "@elixir-cloud/cloud-registry/providers";
5+
import { Building2, Clock, Settings, Tag } from "lucide-react";
6+
7+
type Props = {
8+
serviceInfo: Service;
9+
};
10+
11+
function ServiceInfoCardContent({ serviceInfo }: Props) {
12+
if (serviceInfo === undefined) return null;
13+
14+
return (
15+
<CardContent className="space-y-6">
16+
<p className="text-sm text-slate-600 leading-relaxed">{serviceInfo.description}</p>
17+
18+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
19+
<div className="space-y-2">
20+
<div className="flex items-center gap-2 text-slate-500">
21+
<Building2 className="w-4 h-4" />
22+
<span className="font-medium">Organization</span>
23+
</div>
24+
<Tooltip>
25+
<TooltipTrigger asChild>
26+
<a
27+
href={serviceInfo.organization.url}
28+
className="text-blue-600 hover:underline block pl-6 max-w-max"
29+
>
30+
{serviceInfo.organization.name}
31+
</a>
32+
</TooltipTrigger>
33+
<TooltipContent>{serviceInfo.organization.url}</TooltipContent>
34+
</Tooltip>
35+
</div>
36+
37+
<div className="space-y-2">
38+
<div className="flex items-center gap-2 text-slate-500">
39+
<Tag className="w-4 h-4" />
40+
<span className="font-medium">Build Version</span>
41+
</div>
42+
<code className="text-xs bg-slate-100 px-2 py-1 rounded ml-6 text-slate-800">
43+
{serviceInfo.version}
44+
</code>
45+
</div>
46+
47+
<Separator className="md:col-span-2" />
48+
49+
<div className="space-y-3">
50+
<div className="flex items-center gap-2 text-slate-500 mb-1">
51+
<Settings className="w-4 h-4" />
52+
<span className="font-medium">Registry Type</span>
53+
</div>
54+
<ul className="pl-6 space-y-1 text-xs">
55+
<li>
56+
<span className="text-slate-400">Artifact:</span>{" "}
57+
{serviceInfo.type.artifact}
58+
</li>
59+
<li>
60+
<span className="text-slate-400">Group:</span> {serviceInfo.type.group}
61+
</li>
62+
<li>
63+
<span className="text-slate-400">Spec Version:</span>
64+
{serviceInfo.type.version}
65+
</li>
66+
</ul>
67+
</div>
68+
69+
<div className="space-y-3">
70+
<div className="flex items-center gap-2 text-slate-500 mb-1">
71+
<Clock className="w-4 h-4" />
72+
<span className="font-medium">Lifecycle</span>
73+
</div>
74+
<div className="pl-6 text-xs text-slate-600 space-y-0.5">
75+
<p>Created At: {new Date(serviceInfo.createdAt).toLocaleString()}</p>
76+
<p>Updated At: {new Date(serviceInfo.updatedAt).toLocaleString()}</p>
77+
<p>Environment: {serviceInfo.environment.toUpperCase()}</p>
78+
</div>
79+
</div>
80+
</div>
81+
</CardContent>
82+
);
83+
}
84+
85+
export default ServiceInfoCardContent;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client";
2+
import { Badge } from "@/components/ui/badge";
3+
import { CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
5+
import { Service } from "@elixir-cloud/cloud-registry/providers";
6+
import { BookOpen, ExternalLink } from "lucide-react";
7+
8+
type Props = {
9+
serviceInfo: Service;
10+
};
11+
12+
function ServiceInfoCardHeader({ serviceInfo }: Props) {
13+
if (serviceInfo === undefined) return null;
14+
return (
15+
<CardHeader className="pb-3">
16+
<div className="flex justify-between items-start">
17+
<div>
18+
<CardTitle className="text-xl font-bold flex items-center gap-2">
19+
{serviceInfo.name}
20+
<Badge
21+
variant={serviceInfo.environment === "dev" ? "outline" : "default"}
22+
className="capitalize"
23+
>
24+
{serviceInfo.environment}
25+
</Badge>
26+
</CardTitle>
27+
<CardDescription className="font-mono text-xs mt-1">
28+
ID: {serviceInfo.id}
29+
</CardDescription>
30+
</div>
31+
<div className="flex gap-2">
32+
<Tooltip>
33+
<TooltipTrigger asChild>
34+
<a
35+
href={serviceInfo.documentationUrl}
36+
target="_blank"
37+
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
38+
>
39+
<BookOpen className="w-4 h-4 text-slate-500" />
40+
</a>
41+
</TooltipTrigger>
42+
<TooltipContent>Documentation Url</TooltipContent>
43+
</Tooltip>
44+
<Tooltip>
45+
<TooltipTrigger asChild>
46+
<a
47+
href={serviceInfo.contactUrl}
48+
target="_blank"
49+
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
50+
>
51+
<ExternalLink className="w-4 h-4 text-slate-500" />
52+
</a>
53+
</TooltipTrigger>
54+
<TooltipContent>Contact Url</TooltipContent>
55+
</Tooltip>
56+
</div>
57+
</div>
58+
</CardHeader>
59+
);
60+
}
61+
62+
export default ServiceInfoCardHeader;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Skeleton } from "@/components/ui/skeleton";
2+
3+
function CardSkeleton() {
4+
return <Skeleton className="w-full min-h-80 max-w-4xl mx-auto mt-4 md:mt-10" />;
5+
}
6+
7+
export default CardSkeleton;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ServiceInfoCardContent from "./card-content";
2+
import ServiceInfoCardHeader from "./card-header";
3+
import CardSkeleton from "./card-skeleton";
4+
5+
export { CardSkeleton, ServiceInfoCardContent, ServiceInfoCardHeader };
Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
1+
"use client";
2+
import { Card } from "@/components/ui/card";
3+
import { useCloudRegistry } from "@/hooks";
4+
import { Service } from "@elixir-cloud/cloud-registry/providers";
5+
import { useCallback, useEffect, useState } from "react";
6+
import { toast } from "sonner";
7+
import { CardSkeleton, ServiceInfoCardContent, ServiceInfoCardHeader } from "./_components";
8+
19
function Page() {
2-
return <div>sdaasd</div>;
10+
const { cloudRegistryProvider } = useCloudRegistry();
11+
const [serviceInfo, setServiceInfo] = useState<Service>();
12+
const [error, setError] = useState(false);
13+
const [loading, setLoading] = useState(true);
14+
15+
const fetchServiceInfo = useCallback(() => {
16+
cloudRegistryProvider
17+
?.getServiceInfo()
18+
.then((data) => setServiceInfo(data))
19+
.catch(() => {
20+
toast.error("Could not fetch the service info.");
21+
setError(true);
22+
})
23+
.finally(() => setLoading(false));
24+
}, [cloudRegistryProvider]);
25+
26+
useEffect(() => {
27+
fetchServiceInfo();
28+
}, [fetchServiceInfo]);
29+
30+
if (loading) return <CardSkeleton />;
31+
32+
if (error)
33+
return (
34+
<div className="w-full h-full overflow-hidden">
35+
<p className="align-middle text-muted-foreground">
36+
Could not fetch the service info
37+
</p>
38+
</div>
39+
);
40+
41+
return (
42+
<Card className="w-full max-w-4xl mx-auto mt-4 md:mt-10">
43+
<ServiceInfoCardHeader serviceInfo={serviceInfo} />
44+
<ServiceInfoCardContent serviceInfo={serviceInfo} />
45+
</Card>
46+
);
347
}
448

549
export default Page;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
DropdownMenu,
4+
DropdownMenuContent,
5+
DropdownMenuItem,
6+
DropdownMenuSeparator,
7+
DropdownMenuTrigger,
8+
} from "@/components/ui/dropdown-menu";
9+
import { AuthenticatedCloudRegistryProvider } from "@/lib/authenticated-cloud-registry";
10+
import { ExternalService } from "@elixir-cloud/cloud-registry/providers";
11+
import { MoreHorizontalIcon } from "lucide-react";
12+
import Link from "next/link";
13+
import { toast } from "sonner";
14+
15+
type Props = {
16+
cloudRegistryProvider: AuthenticatedCloudRegistryProvider | null;
17+
rowData: ExternalService;
18+
fetchServices: () => void;
19+
onViewDetails: (service: ExternalService) => void;
20+
};
21+
22+
function ActionDropdown(props: Props) {
23+
const { cloudRegistryProvider, rowData, fetchServices, onViewDetails } = props;
24+
25+
const handleServiceDelete = async (id: string) => {
26+
if (!cloudRegistryProvider) return;
27+
28+
const promise = cloudRegistryProvider.deleteService(id);
29+
30+
toast.promise(promise, {
31+
loading: `Deleting service with ID - ${id}`,
32+
success: (response) => `Deleted service with ID - ${response}`,
33+
error: "Cannot delete service currently.",
34+
});
35+
36+
try {
37+
await promise;
38+
await fetchServices();
39+
} catch {
40+
toast.error("Cannot update the service list.");
41+
}
42+
};
43+
44+
return (
45+
<DropdownMenu>
46+
<DropdownMenuTrigger asChild>
47+
<Button variant="ghost" size="icon" className="size-8">
48+
<MoreHorizontalIcon />
49+
<span className="sr-only">Open menu</span>
50+
</Button>
51+
</DropdownMenuTrigger>
52+
<DropdownMenuContent align="end">
53+
<DropdownMenuItem onClick={() => onViewDetails(rowData)}>View</DropdownMenuItem>
54+
<DropdownMenuItem asChild>
55+
<Link href={`/dashboard/update-service/${rowData.id}`}>Edit</Link>
56+
</DropdownMenuItem>
57+
<DropdownMenuSeparator />
58+
<DropdownMenuItem
59+
variant="destructive"
60+
onClick={() => handleServiceDelete(rowData.id)}
61+
>
62+
Delete
63+
</DropdownMenuItem>
64+
</DropdownMenuContent>
65+
</DropdownMenu>
66+
);
67+
}
68+
69+
export default ActionDropdown;

0 commit comments

Comments
 (0)