diff --git a/apps/dokploy/components/dashboard/application/icon/show-icon-settings.tsx b/apps/dokploy/components/dashboard/application/icon/show-icon-settings.tsx new file mode 100644 index 000000000..dd71a7617 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/icon/show-icon-settings.tsx @@ -0,0 +1,271 @@ +import { Search, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Dropzone } from "@/components/ui/dropzone"; +import { Input } from "@/components/ui/input"; +import iconNames from "@/lib/icons.json"; +import { api } from "@/utils/api"; + +interface ShowIconSettingsProps { + applicationId: string; +} + +export const ShowIconSettings = ({ applicationId }: ShowIconSettingsProps) => { + const [uploadedIcon, setUploadedIcon] = useState(null); + const [iconSearchQuery, setIconSearchQuery] = useState(""); + const [iconsToShow, setIconsToShow] = useState(24); + + const popularIcons = (iconNames as string[]).sort(); + const filteredIcons = popularIcons.filter((icon) => + icon.toLowerCase().includes(iconSearchQuery.toLowerCase()), + ); + const displayedIcons = filteredIcons.slice(0, iconsToShow); + const hasMoreIcons = filteredIcons.length > iconsToShow; + + const { data } = api.application.one.useQuery( + { applicationId }, + { refetchInterval: 5000 }, + ); + const utils = api.useUtils(); + const { mutateAsync: updateApplication } = + api.application.update.useMutation(); + const { mutateAsync: fetchIcon } = api.application.fetchIcon.useMutation(); + + useEffect(() => { + if (data?.icon) { + setUploadedIcon(data.icon); + } else { + setUploadedIcon(null); + } + }, [data?.icon]); + + useEffect(() => { + setIconsToShow(24); + }, [iconSearchQuery]); + + const handleIconSelect = async (iconName: string) => { + try { + const result = await fetchIcon({ iconName }); + setUploadedIcon(result.icon); + await updateApplication({ + applicationId, + icon: result.icon, + }); + toast.success("Icon saved successfully"); + await utils.application.one.invalidate({ applicationId }); + } catch (error) { + toast.error("Error loading icon"); + } + }; + + return ( +
+ {uploadedIcon && ( +
+ {/* biome-ignore lint/performance/noImgElement: uploaded icon is data URL; Next/Image not used for preview */} + Uploaded icon +
+

Icon uploaded

+

+ This icon will appear in service cards +

+
+ +
+ )} + +
+
+ + setIconSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+ {displayedIcons.length === 0 ? ( +
+ No icons found +
+ ) : ( + <> +
+ {displayedIcons.map((iconName) => ( + + ))} +
+ {hasMoreIcons && ( +
+ +
+ )} + + )} +
+ +
+

+ or upload a custom icon +

+
+ { + if (!files || files.length === 0) return; + const file = files[0]; + if (!file) return; + + const fileToProcess: File = file; + + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/svg+xml", + ]; + const fileExtension = fileToProcess.name + .split(".") + .pop() + ?.toLowerCase(); + const allowedExtensions = [ + "jpg", + "jpeg", + "png", + "svg", + ]; + + if ( + !allowedTypes.includes(fileToProcess.type) && + !allowedExtensions.includes( + fileExtension || "", + ) + ) { + toast.error( + "Only JPG, JPEG, PNG, and SVG files are allowed", + ); + return; + } + + if (fileToProcess.size > 2 * 1024 * 1024) { + toast.error( + "Image size must be less than 2MB", + ); + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const result = event.target?.result as string; + setUploadedIcon(result); + try { + await updateApplication({ + applicationId, + icon: result, + }); + toast.success("Icon saved!"); + await utils.application.one.invalidate({ + applicationId, + }); + } catch (error) { + toast.error("Error saving icon"); + setUploadedIcon(null); + } + }; + reader.readAsDataURL(fileToProcess); + }} + classNameWrapper="border-2 border-dashed border-border hover:border-primary bg-muted/30 hover:bg-muted/50 transition-all rounded-lg" + /> +
+
+ Supported formats: JPG, JPEG, PNG, SVG (max 2MB) +
+
+ +
+
+
+ Icons by + + gilbarbara/logos + +
+
+ Developer: + + Statsly + +
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx index 8fecacb43..969a4159c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx @@ -1,7 +1,7 @@ import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import copy from "copy-to-clipboard"; -import { GlobeIcon, HelpCircle, Search, ServerOff, X } from "lucide-react"; +import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, @@ -29,6 +29,7 @@ import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments"; import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { UpdateApplication } from "@/components/dashboard/application/update-application"; +import { ShowIconSettings } from "@/components/dashboard/application/icon/show-icon-settings"; import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups"; import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring"; @@ -45,8 +46,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Dropzone } from "@/components/ui/dropzone"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -56,7 +55,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { UseKeyboardNav } from "@/hooks/use-keyboard-nav"; -import iconNames from "@/lib/icons.json"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; @@ -79,41 +77,6 @@ const Service = ( const router = useRouter(); const { projectId, environmentId } = router.query; const [tab, setTab] = useState(activeTab); - const [uploadedIcon, setUploadedIcon] = useState(null); - const [iconSearchQuery, setIconSearchQuery] = useState(""); - const [iconsToShow, setIconsToShow] = useState(24); - - const popularIcons = (iconNames as string[]).sort(); - - const filteredIcons = popularIcons.filter((icon) => - icon.toLowerCase().includes(iconSearchQuery.toLowerCase()), - ); - - const displayedIcons = filteredIcons.slice(0, iconsToShow); - const hasMoreIcons = filteredIcons.length > iconsToShow; - - useEffect(() => { - setIconsToShow(24); - }, [iconSearchQuery]); - - const { mutateAsync: fetchIcon } = api.application.fetchIcon.useMutation(); - - const handleIconSelect = async (iconName: string) => { - try { - // Fetch like this so no CORS issues appear - const result = await fetchIcon({ iconName }); - - setUploadedIcon(result.icon); - await updateApplication({ - applicationId, - icon: result.icon, - }); - toast.success("Icon saved successfully"); - await utils.application.one.invalidate({ applicationId }); - } catch (error) { - toast.error("Error loading icon"); - } - }; useEffect(() => { if (router.query.tab) { @@ -128,24 +91,6 @@ const Service = ( }, ); - const utils = api.useUtils(); - const { mutateAsync: updateApplication } = - api.application.update.useMutation(); - - useEffect(() => { - if (data) { - console.log("Application data loaded:", { - icon: data.icon, - hasIcon: !!data.icon, - }); - if (data.icon) { - setUploadedIcon(data.icon); - } else { - setUploadedIcon(null); - } - } - }, [data]); - const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: auth } = api.user.get.useQuery(); @@ -444,213 +389,7 @@ const Service = ( -
- {uploadedIcon && ( -
- {/* biome-ignore lint/performance/noImgElement: uploaded icon is data URL; Next/Image not used for preview */} - Uploaded icon -
-

Icon uploaded

-

- This icon will appear in service cards -

-
- -
- )} - -
-
- - setIconSearchQuery(e.target.value)} - className="pl-9" - /> -
- -
- {displayedIcons.length === 0 ? ( -
- No icons found -
- ) : ( - <> -
- {displayedIcons.map((iconName) => ( - - ))} -
- {hasMoreIcons && ( -
- -
- )} - - )} -
- -
-

- or upload a custom icon -

-
- { - if (!files || files.length === 0) return; - const file = files[0]; - if (!file) return; - - const fileToProcess: File = file; - - const allowedTypes = [ - "image/jpeg", - "image/jpg", - "image/png", - "image/svg+xml", - ]; - const fileExtension = fileToProcess.name - .split(".") - .pop() - ?.toLowerCase(); - const allowedExtensions = [ - "jpg", - "jpeg", - "png", - "svg", - ]; - - if ( - !allowedTypes.includes(fileToProcess.type) && - !allowedExtensions.includes( - fileExtension || "", - ) - ) { - toast.error( - "Only JPG, JPEG, PNG, and SVG files are allowed", - ); - return; - } - - if (fileToProcess.size > 2 * 1024 * 1024) { - toast.error( - "Image size must be less than 2MB", - ); - return; - } - - const reader = new FileReader(); - reader.onload = async (event) => { - const result = event.target?.result as string; - setUploadedIcon(result); - try { - await updateApplication({ - applicationId, - icon: result, - }); - toast.success("Icon saved!"); - await utils.application.one.invalidate({ - applicationId, - }); - } catch (error) { - toast.error("Error saving icon"); - setUploadedIcon(null); - } - }; - reader.readAsDataURL(fileToProcess); - }} - classNameWrapper="border-2 border-dashed border-border hover:border-primary bg-muted/30 hover:bg-muted/50 transition-all rounded-lg" - /> -
-
- Supported formats: JPG, JPEG, PNG, SVG (max 2MB) -
-
- -
-
-
- Icons by - - gilbarbara/logos - -
-
- Developer: - - Statsly - -
-
-
-
-
+
)}