mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-27 08:45:33 +02:00
chore(ui): apply Biome format to time badge and headers
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
Search,
|
Search,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -16,41 +16,41 @@ import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
@@ -58,455 +58,470 @@ import { ProjectEnvironment } from "./project-environment";
|
|||||||
import { TimeBadge } from "@/components/ui/time-badge";
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [sortBy, setSortBy] = useState<string>(() => {
|
const [sortBy, setSortBy] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return localStorage.getItem("projectsSort") || "createdAt-desc";
|
return localStorage.getItem("projectsSort") || "createdAt-desc";
|
||||||
}
|
}
|
||||||
return "createdAt-desc";
|
return "createdAt-desc";
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("projectsSort", sortBy);
|
localStorage.setItem("projectsSort", sortBy);
|
||||||
}, [sortBy]);
|
}, [sortBy]);
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
// First filter by search query
|
// First filter by search query
|
||||||
const filtered = data.filter(
|
const filtered = data.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
project.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then sort the filtered results
|
// Then sort the filtered results
|
||||||
const [field, direction] = sortBy.split("-");
|
const [field, direction] = sortBy.split("-");
|
||||||
return [...filtered].sort((a, b) => {
|
return [...filtered].sort((a, b) => {
|
||||||
let comparison = 0;
|
let comparison = 0;
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case "name":
|
case "name":
|
||||||
comparison = a.name.localeCompare(b.name);
|
comparison = a.name.localeCompare(b.name);
|
||||||
break;
|
break;
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
comparison =
|
comparison =
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
break;
|
break;
|
||||||
case "services": {
|
case "services": {
|
||||||
const aTotalServices = a.environments.reduce((total, env) => {
|
const aTotalServices = a.environments.reduce((total, env) => {
|
||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
(env.postgres?.length || 0) +
|
(env.postgres?.length || 0) +
|
||||||
(env.redis?.length || 0) +
|
(env.redis?.length || 0) +
|
||||||
(env.compose?.length || 0)
|
(env.compose?.length || 0)
|
||||||
);
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
const bTotalServices = b.environments.reduce((total, env) => {
|
const bTotalServices = b.environments.reduce((total, env) => {
|
||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
(env.postgres?.length || 0) +
|
(env.postgres?.length || 0) +
|
||||||
(env.redis?.length || 0) +
|
(env.redis?.length || 0) +
|
||||||
(env.compose?.length || 0)
|
(env.compose?.length || 0)
|
||||||
);
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
comparison = aTotalServices - bTotalServices;
|
comparison = aTotalServices - bTotalServices;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
comparison = 0;
|
comparison = 0;
|
||||||
}
|
}
|
||||||
return direction === "asc" ? comparison : -comparison;
|
return direction === "asc" ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
}, [data, searchQuery, sortBy]);
|
}, [data, searchQuery, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-5 right-5">
|
<div className="absolute top-5 right-5">
|
||||||
<TimeBadge />
|
<TimeBadge />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
|
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
|
||||||
<CardHeader className="p-0">
|
<CardHeader className="p-0">
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
<FolderInput className="size-6 text-muted-foreground self-center" />
|
<FolderInput className="size-6 text-muted-foreground self-center" />
|
||||||
Projects
|
Projects
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]">
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]">
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
<Loader2 className="animate-spin size-4" />
|
<Loader2 className="animate-spin size-4" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
||||||
<div className="flex-1 relative max-sm:w-full">
|
<div className="flex-1 relative max-sm:w-full">
|
||||||
<FocusShortcutInput
|
<FocusShortcutInput
|
||||||
placeholder="Filter projects..."
|
placeholder="Filter projects..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Sort by..." />
|
<SelectValue placeholder="Sort by..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||||
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||||
<SelectItem value="createdAt-desc">
|
<SelectItem value="createdAt-desc">
|
||||||
Newest first
|
Newest first
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="createdAt-asc">
|
<SelectItem value="createdAt-asc">
|
||||||
Oldest first
|
Oldest first
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="services-desc">
|
<SelectItem value="services-desc">
|
||||||
Most services
|
Most services
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="services-asc">
|
<SelectItem value="services-asc">
|
||||||
Least services
|
Least services
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filteredProjects?.length === 0 && (
|
{filteredProjects?.length === 0 && (
|
||||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
||||||
<FolderInput className="size-8 self-center text-muted-foreground" />
|
<FolderInput className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-center font-medium text-muted-foreground">
|
<span className="text-center font-medium text-muted-foreground">
|
||||||
No projects found
|
No projects found
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
||||||
{filteredProjects?.map((project) => {
|
{filteredProjects?.map((project) => {
|
||||||
const emptyServices = project?.environments
|
const emptyServices = project?.environments
|
||||||
.map(
|
.map(
|
||||||
(env) =>
|
(env) =>
|
||||||
env.applications.length === 0 &&
|
env.applications.length === 0 &&
|
||||||
env.mariadb.length === 0 &&
|
env.mariadb.length === 0 &&
|
||||||
env.mongo.length === 0 &&
|
env.mongo.length === 0 &&
|
||||||
env.mysql.length === 0 &&
|
env.mysql.length === 0 &&
|
||||||
env.postgres.length === 0 &&
|
env.postgres.length === 0 &&
|
||||||
env.redis.length === 0 &&
|
env.redis.length === 0 &&
|
||||||
env.applications.length === 0 &&
|
env.applications.length === 0 &&
|
||||||
env.compose.length === 0
|
env.compose.length === 0,
|
||||||
)
|
)
|
||||||
.every(Boolean);
|
.every(Boolean);
|
||||||
|
|
||||||
const totalServices = project?.environments
|
const totalServices = project?.environments
|
||||||
.map(
|
.map(
|
||||||
(env) =>
|
(env) =>
|
||||||
env.mariadb.length +
|
env.mariadb.length +
|
||||||
env.mongo.length +
|
env.mongo.length +
|
||||||
env.mysql.length +
|
env.mysql.length +
|
||||||
env.postgres.length +
|
env.postgres.length +
|
||||||
env.redis.length +
|
env.redis.length +
|
||||||
env.applications.length +
|
env.applications.length +
|
||||||
env.compose.length
|
env.compose.length,
|
||||||
)
|
)
|
||||||
.reduce((acc, curr) => acc + curr, 0);
|
.reduce((acc, curr) => acc + curr, 0);
|
||||||
|
|
||||||
const haveServicesWithDomains = project?.environments
|
const haveServicesWithDomains = project?.environments
|
||||||
.map(
|
.map(
|
||||||
(env) =>
|
(env) =>
|
||||||
env.applications.length > 0 ||
|
env.applications.length > 0 ||
|
||||||
env.compose.length > 0
|
env.compose.length > 0,
|
||||||
)
|
)
|
||||||
.some(Boolean);
|
.some(Boolean);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={project.projectId}
|
key={project.projectId}
|
||||||
className="w-full lg:max-w-md">
|
className="w-full lg:max-w-md"
|
||||||
<Link
|
>
|
||||||
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}>
|
<Link
|
||||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}
|
||||||
{haveServicesWithDomains ? (
|
>
|
||||||
<DropdownMenu>
|
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||||
<DropdownMenuTrigger asChild>
|
{haveServicesWithDomains ? (
|
||||||
<Button
|
<DropdownMenu>
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
<DropdownMenuTrigger asChild>
|
||||||
size="sm"
|
<Button
|
||||||
variant="default">
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
size="sm"
|
||||||
</Button>
|
variant="default"
|
||||||
</DropdownMenuTrigger>
|
>
|
||||||
<DropdownMenuContent
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
</Button>
|
||||||
onClick={(e) => e.stopPropagation()}>
|
</DropdownMenuTrigger>
|
||||||
{project.environments.some(
|
<DropdownMenuContent
|
||||||
(env) => env.applications.length > 0
|
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
||||||
) && (
|
onClick={(e) => e.stopPropagation()}
|
||||||
<DropdownMenuGroup>
|
>
|
||||||
<DropdownMenuLabel>
|
{project.environments.some(
|
||||||
Applications
|
(env) => env.applications.length > 0,
|
||||||
</DropdownMenuLabel>
|
) && (
|
||||||
{project.environments.map((env) =>
|
<DropdownMenuGroup>
|
||||||
env.applications.map((app) => (
|
<DropdownMenuLabel>
|
||||||
<div key={app.applicationId}>
|
Applications
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuGroup>
|
{project.environments.map((env) =>
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
env.applications.map((app) => (
|
||||||
{app.name}
|
<div key={app.applicationId}>
|
||||||
<StatusTooltip
|
<DropdownMenuSeparator />
|
||||||
status={
|
<DropdownMenuGroup>
|
||||||
app.applicationStatus
|
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||||
}
|
{app.name}
|
||||||
/>
|
<StatusTooltip
|
||||||
</DropdownMenuLabel>
|
status={
|
||||||
<DropdownMenuSeparator />
|
app.applicationStatus
|
||||||
{app.domains.map((domain) => (
|
}
|
||||||
<DropdownMenuItem
|
/>
|
||||||
key={domain.domainId}
|
</DropdownMenuLabel>
|
||||||
asChild>
|
<DropdownMenuSeparator />
|
||||||
<Link
|
{app.domains.map((domain) => (
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
<DropdownMenuItem
|
||||||
target="_blank"
|
key={domain.domainId}
|
||||||
href={`${
|
asChild
|
||||||
domain.https
|
>
|
||||||
? "https"
|
<Link
|
||||||
: "http"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
}://${domain.host}${
|
target="_blank"
|
||||||
domain.path
|
href={`${
|
||||||
}`}>
|
domain.https
|
||||||
<span className="truncate">
|
? "https"
|
||||||
{domain.host}
|
: "http"
|
||||||
</span>
|
}://${domain.host}${
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
domain.path
|
||||||
</Link>
|
}`}
|
||||||
</DropdownMenuItem>
|
>
|
||||||
))}
|
<span className="truncate">
|
||||||
</DropdownMenuGroup>
|
{domain.host}
|
||||||
</div>
|
</span>
|
||||||
))
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
)}
|
</Link>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuItem>
|
||||||
)}
|
))}
|
||||||
{project.environments.some(
|
</DropdownMenuGroup>
|
||||||
(env) => env.compose.length > 0
|
</div>
|
||||||
) && (
|
)),
|
||||||
<DropdownMenuGroup>
|
)}
|
||||||
<DropdownMenuLabel>
|
</DropdownMenuGroup>
|
||||||
Compose
|
)}
|
||||||
</DropdownMenuLabel>
|
{project.environments.some(
|
||||||
{project.environments.map((env) =>
|
(env) => env.compose.length > 0,
|
||||||
env.compose.map((comp) => (
|
) && (
|
||||||
<div key={comp.composeId}>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuLabel>
|
||||||
<DropdownMenuGroup>
|
Compose
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
</DropdownMenuLabel>
|
||||||
{comp.name}
|
{project.environments.map((env) =>
|
||||||
<StatusTooltip
|
env.compose.map((comp) => (
|
||||||
status={comp.composeStatus}
|
<div key={comp.composeId}>
|
||||||
/>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||||
{comp.domains.map((domain) => (
|
{comp.name}
|
||||||
<DropdownMenuItem
|
<StatusTooltip
|
||||||
key={domain.domainId}
|
status={comp.composeStatus}
|
||||||
asChild>
|
/>
|
||||||
<Link
|
</DropdownMenuLabel>
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
<DropdownMenuSeparator />
|
||||||
target="_blank"
|
{comp.domains.map((domain) => (
|
||||||
href={`${
|
<DropdownMenuItem
|
||||||
domain.https
|
key={domain.domainId}
|
||||||
? "https"
|
asChild
|
||||||
: "http"
|
>
|
||||||
}://${domain.host}${
|
<Link
|
||||||
domain.path
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
}`}>
|
target="_blank"
|
||||||
<span className="truncate">
|
href={`${
|
||||||
{domain.host}
|
domain.https
|
||||||
</span>
|
? "https"
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
: "http"
|
||||||
</Link>
|
}://${domain.host}${
|
||||||
</DropdownMenuItem>
|
domain.path
|
||||||
))}
|
}`}
|
||||||
</DropdownMenuGroup>
|
>
|
||||||
</div>
|
<span className="truncate">
|
||||||
))
|
{domain.host}
|
||||||
)}
|
</span>
|
||||||
</DropdownMenuGroup>
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
)}
|
</Link>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
))}
|
||||||
) : null}
|
</DropdownMenuGroup>
|
||||||
<CardHeader>
|
</div>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
)),
|
||||||
<span className="flex flex-col gap-1.5">
|
)}
|
||||||
<div className="flex items-center gap-2">
|
</DropdownMenuGroup>
|
||||||
<BookIcon className="size-4 text-muted-foreground" />
|
)}
|
||||||
<span className="text-base font-medium leading-none">
|
</DropdownMenuContent>
|
||||||
{project.name}
|
</DropdownMenu>
|
||||||
</span>
|
) : null}
|
||||||
</div>
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
|
<span className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookIcon className="size-4 text-muted-foreground" />
|
||||||
|
<span className="text-base font-medium leading-none">
|
||||||
|
{project.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{project.description}
|
{project.description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex self-start space-x-1">
|
<div className="flex self-start space-x-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="px-2">
|
className="px-2"
|
||||||
<MoreHorizontalIcon className="size-5" />
|
>
|
||||||
</Button>
|
<MoreHorizontalIcon className="size-5" />
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent
|
</DropdownMenuTrigger>
|
||||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
|
<DropdownMenuContent
|
||||||
onClick={(e) => e.stopPropagation()}>
|
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
|
||||||
<DropdownMenuLabel className="font-normal">
|
onClick={(e) => e.stopPropagation()}
|
||||||
Actions
|
>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div
|
Actions
|
||||||
onClick={(e) => e.stopPropagation()}>
|
</DropdownMenuLabel>
|
||||||
<ProjectEnvironment
|
<div
|
||||||
projectId={project.projectId}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
>
|
||||||
</div>
|
<ProjectEnvironment
|
||||||
<div
|
projectId={project.projectId}
|
||||||
onClick={(e) => e.stopPropagation()}>
|
/>
|
||||||
<HandleProject
|
</div>
|
||||||
projectId={project.projectId}
|
<div
|
||||||
/>
|
onClick={(e) => e.stopPropagation()}
|
||||||
</div>
|
>
|
||||||
|
<HandleProject
|
||||||
|
projectId={project.projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{(auth?.role === "owner" ||
|
>
|
||||||
auth?.canDeleteProjects) && (
|
{(auth?.role === "owner" ||
|
||||||
<AlertDialog>
|
auth?.canDeleteProjects) && (
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialog>
|
||||||
<DropdownMenuItem
|
<AlertDialogTrigger className="w-full">
|
||||||
className="w-full cursor-pointer space-x-3"
|
<DropdownMenuItem
|
||||||
onSelect={(e) =>
|
className="w-full cursor-pointer space-x-3"
|
||||||
e.preventDefault()
|
onSelect={(e) =>
|
||||||
}>
|
e.preventDefault()
|
||||||
<TrashIcon className="size-4" />
|
}
|
||||||
<span>Delete</span>
|
>
|
||||||
</DropdownMenuItem>
|
<TrashIcon className="size-4" />
|
||||||
</AlertDialogTrigger>
|
<span>Delete</span>
|
||||||
<AlertDialogContent>
|
</DropdownMenuItem>
|
||||||
<AlertDialogHeader>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogTitle>
|
<AlertDialogContent>
|
||||||
Are you sure to delete this
|
<AlertDialogHeader>
|
||||||
project?
|
<AlertDialogTitle>
|
||||||
</AlertDialogTitle>
|
Are you sure to delete this
|
||||||
{!emptyServices ? (
|
project?
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
</AlertDialogTitle>
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
{!emptyServices ? (
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||||
You have active
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
services, please delete
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
them first
|
You have active
|
||||||
</span>
|
services, please delete
|
||||||
</div>
|
them first
|
||||||
) : (
|
</span>
|
||||||
<AlertDialogDescription>
|
</div>
|
||||||
This action cannot be
|
) : (
|
||||||
undone
|
<AlertDialogDescription>
|
||||||
</AlertDialogDescription>
|
This action cannot be
|
||||||
)}
|
undone
|
||||||
</AlertDialogHeader>
|
</AlertDialogDescription>
|
||||||
<AlertDialogFooter>
|
)}
|
||||||
<AlertDialogCancel>
|
</AlertDialogHeader>
|
||||||
Cancel
|
<AlertDialogFooter>
|
||||||
</AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
<AlertDialogAction
|
Cancel
|
||||||
disabled={!emptyServices}
|
</AlertDialogCancel>
|
||||||
onClick={async () => {
|
<AlertDialogAction
|
||||||
await mutateAsync({
|
disabled={!emptyServices}
|
||||||
projectId:
|
onClick={async () => {
|
||||||
project.projectId,
|
await mutateAsync({
|
||||||
})
|
projectId:
|
||||||
.then(() => {
|
project.projectId,
|
||||||
toast.success(
|
})
|
||||||
"Project deleted successfully"
|
.then(() => {
|
||||||
);
|
toast.success(
|
||||||
})
|
"Project deleted successfully",
|
||||||
.catch(() => {
|
);
|
||||||
toast.error(
|
})
|
||||||
"Error deleting this project"
|
.catch(() => {
|
||||||
);
|
toast.error(
|
||||||
})
|
"Error deleting this project",
|
||||||
.finally(() => {
|
);
|
||||||
utils.project.all.invalidate();
|
})
|
||||||
});
|
.finally(() => {
|
||||||
}}>
|
utils.project.all.invalidate();
|
||||||
Delete
|
});
|
||||||
</AlertDialogAction>
|
}}
|
||||||
</AlertDialogFooter>
|
>
|
||||||
</AlertDialogContent>
|
Delete
|
||||||
</AlertDialog>
|
</AlertDialogAction>
|
||||||
)}
|
</AlertDialogFooter>
|
||||||
</div>
|
</AlertDialogContent>
|
||||||
</DropdownMenuContent>
|
</AlertDialog>
|
||||||
</DropdownMenu>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</DropdownMenuContent>
|
||||||
</CardHeader>
|
</DropdownMenu>
|
||||||
<CardFooter className="pt-4">
|
</div>
|
||||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
</CardTitle>
|
||||||
<DateTooltip date={project.createdAt}>
|
</CardHeader>
|
||||||
Created
|
<CardFooter className="pt-4">
|
||||||
</DateTooltip>
|
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
<span>
|
<DateTooltip date={project.createdAt}>
|
||||||
{totalServices}{" "}
|
Created
|
||||||
{totalServices === 1
|
</DateTooltip>
|
||||||
? "service"
|
<span>
|
||||||
: "services"}
|
{totalServices}{" "}
|
||||||
</span>
|
{totalServices === 1
|
||||||
</div>
|
? "service"
|
||||||
</CardFooter>
|
: "services"}
|
||||||
</Card>
|
</span>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</CardFooter>
|
||||||
);
|
</Card>
|
||||||
})}
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
)}
|
})}
|
||||||
</CardContent>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
</CardContent>
|
||||||
</>
|
</div>
|
||||||
);
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,57 +4,57 @@ import { useEffect, useState } from "react";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
export function TimeBadge() {
|
export function TimeBadge() {
|
||||||
const { data: serverTime } = api.server.getServerTime.useQuery(undefined, {
|
const { data: serverTime } = api.server.getServerTime.useQuery(undefined, {
|
||||||
refetchInterval: 60000, // Refetch every 60 seconds
|
refetchInterval: 60000, // Refetch every 60 seconds
|
||||||
});
|
});
|
||||||
const [time, setTime] = useState<Date | null>(null);
|
const [time, setTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverTime?.time) {
|
if (serverTime?.time) {
|
||||||
setTime(new Date(serverTime.time));
|
setTime(new Date(serverTime.time));
|
||||||
}
|
}
|
||||||
}, [serverTime]);
|
}, [serverTime]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setTime((prevTime) => {
|
setTime((prevTime) => {
|
||||||
if (!prevTime) return null;
|
if (!prevTime) return null;
|
||||||
const newTime = new Date(prevTime.getTime() + 1000);
|
const newTime = new Date(prevTime.getTime() + 1000);
|
||||||
return newTime;
|
return newTime;
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!time || !serverTime?.timezone) {
|
if (!time || !serverTime?.timezone) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUtcOffset = (timeZone: string) => {
|
const getUtcOffset = (timeZone: string) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
||||||
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
|
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
|
||||||
const sign = offset >= 0 ? "+" : "-";
|
const sign = offset >= 0 ? "+" : "-";
|
||||||
const hours = Math.floor(Math.abs(offset));
|
const hours = Math.floor(Math.abs(offset));
|
||||||
const minutes = (Math.abs(offset) * 60) % 60;
|
const minutes = (Math.abs(offset) * 60) % 60;
|
||||||
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
|
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}`;
|
.padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
||||||
<span className="hidden sm:inline">Server Time:</span>
|
<span className="hidden sm:inline">Server Time:</span>
|
||||||
<span className="font-medium tabular-nums">
|
<span className="font-medium tabular-nums">
|
||||||
{time.toLocaleTimeString()}
|
{time.toLocaleTimeString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden sm:inline text-muted-foreground">
|
<span className="hidden sm:inline text-muted-foreground">
|
||||||
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user