mirror of
				https://github.com/gethomepage/homepage.git
				synced 2025-11-01 19:17:03 -04:00 
			
		
		
		
	Custom JS and CSS (#1950)
* First commit for custom styles and JS * Adjusted classes * Added ids and classes for services and bookmarks * Apply suggestions from code review * Remove mime dependency * Update settings.json * Detect custom css / js changes, no refresh * Added preload to custom scripts and styles so they can load earlier * Added data attribute name for bookmarks too * Update [path].js * code style, revert some pointer changes --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									0741ef0427
								
							
						
					
					
						commit
						b39c79bea1
					
				| @ -13,6 +13,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) { | ||||
|     <div | ||||
|       key={bookmarks.name} | ||||
|       className={classNames( | ||||
|         "bookmark-group", | ||||
|         layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6", | ||||
|         layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1" | ||||
|       )} | ||||
| @ -23,11 +24,11 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) { | ||||
|             {layout?.header !== false && ( | ||||
|               <Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group"> | ||||
|                 {layout?.icon && ( | ||||
|                   <div className="flex-shrink-0 mr-2 w-7 h-7"> | ||||
|                   <div className="flex-shrink-0 mr-2 w-7 h-7 bookmark-group-icon"> | ||||
|                     <ResolvedIcon icon={layout.icon} /> | ||||
|                   </div> | ||||
|                 )} | ||||
|                 <h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{bookmarks.name}</h2> | ||||
|                 <h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium bookmark-group-name">{bookmarks.name}</h2> | ||||
|                 <MdKeyboardArrowDown | ||||
|                   className={classNames( | ||||
|                     disableCollapse ? "hidden" : "", | ||||
|  | ||||
| @ -9,7 +9,7 @@ export default function Item({ bookmark }) { | ||||
|   const { settings } = useContext(SettingsContext); | ||||
| 
 | ||||
|   return ( | ||||
|     <li key={bookmark.name}> | ||||
|     <li key={bookmark.name} id={bookmark.id} className="bookmark" data-name={bookmark.name}> | ||||
|       <a | ||||
|         href={bookmark.href} | ||||
|         title={bookmark.name} | ||||
| @ -20,7 +20,7 @@ export default function Item({ bookmark }) { | ||||
|         )} | ||||
|       > | ||||
|         <div className="flex"> | ||||
|           <div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md"> | ||||
|           <div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md bookmark-icon"> | ||||
|             {bookmark.icon &&  | ||||
|               <div className="flex-shrink-0 w-5 h-5"> | ||||
|                 <ResolvedIcon icon={bookmark.icon} alt={bookmark.abbr} /> | ||||
| @ -28,9 +28,9 @@ export default function Item({ bookmark }) { | ||||
|             } | ||||
|             {!bookmark.icon && bookmark.abbr} | ||||
|           </div> | ||||
|           <div className="flex-1 flex items-center justify-between rounded-r-md"> | ||||
|             <div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div> | ||||
|             <div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs">{hostname}</div> | ||||
|           <div className="flex-1 flex items-center justify-between rounded-r-md bookmark-text"> | ||||
|             <div className="flex-1 grow pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div> | ||||
|             <div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-hostname">{hostname}</div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </a> | ||||
|  | ||||
| @ -9,7 +9,7 @@ export default function List({ bookmarks, layout }) { | ||||
|     <ul | ||||
|       className={classNames( | ||||
|         layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col", | ||||
|         "mt-3" | ||||
|         "mt-3 bookmark-list" | ||||
|       )} | ||||
|     > | ||||
|       {bookmarks.map((bookmark) => ( | ||||
|  | ||||
							
								
								
									
										10
									
								
								src/components/filecontent.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/components/filecontent.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import useSWR from "swr" | ||||
| 
 | ||||
| export default function FileContent({ path, loadingValue, errorValue, emptyValue = '' }) { | ||||
|   const fetcher = (url) => fetch(url).then((res) => res.text()) | ||||
|   const { data, error, isLoading } = useSWR(`/api/config/${ path }`, fetcher) | ||||
| 
 | ||||
|   if (error) return (errorValue) | ||||
|   if (isLoading) return (loadingValue) | ||||
|   return (data || emptyValue) | ||||
| } | ||||
| @ -14,6 +14,7 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di | ||||
|     <div | ||||
|       key={services.name} | ||||
|       className={classNames( | ||||
|         "services-group", | ||||
|         layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4", | ||||
|         layout?.style !== "row" && fiveColumns ? "3xl:basis-1/5" : "", | ||||
|         layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1", | ||||
| @ -25,11 +26,11 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di | ||||
|           { layout?.header !== false && | ||||
|             <Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group"> | ||||
|               {layout?.icon && | ||||
|                 <div className="flex-shrink-0 mr-2 w-7 h-7"> | ||||
|                 <div className="flex-shrink-0 mr-2 w-7 h-7 service-group-icon"> | ||||
|                   <ResolvedIcon icon={layout.icon} /> | ||||
|                 </div> | ||||
|               } | ||||
|               <h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium">{services.name}</h2> | ||||
|               <h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium service-group-name">{services.name}</h2> | ||||
|               <MdKeyboardArrowDown className={classNames( | ||||
|                 disableCollapse ? 'hidden' : '', | ||||
|                 'transition-all opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl', | ||||
|  | ||||
| @ -29,28 +29,30 @@ export default function Item({ service, group }) { | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   return ( | ||||
|     <li key={service.name}> | ||||
|     <li key={service.name} id={service.id} className="service" data-name={service.name || ""}> | ||||
|       <div | ||||
|         className={classNames( | ||||
|           settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? '-' : ""}${settings.cardBlur}`, | ||||
|           hasLink && "cursor-pointer", | ||||
|           'transition-all h-15 mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip' | ||||
|           "transition-all h-15 mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card" | ||||
|         )} | ||||
|       > | ||||
|         <div className="flex select-none z-0"> | ||||
|         <div className="flex select-none z-0 service-title"> | ||||
|           {service.icon && | ||||
|             (hasLink ? ( | ||||
|               <a | ||||
|                 href={service.href} | ||||
|                 target={service.target ?? settings.target ?? "_blank"} | ||||
|                 rel="noreferrer" | ||||
|                 className="flex-shrink-0 flex items-center justify-center w-12 " | ||||
|                 className="flex-shrink-0 flex items-center justify-center w-12 service-icon" | ||||
|               > | ||||
|                 <ResolvedIcon icon={service.icon} /> | ||||
|               </a> | ||||
|             ) : ( | ||||
|               <div className="flex-shrink-0 flex items-center justify-center w-12 "> | ||||
|               <div className="flex-shrink-0 flex items-center justify-center w-12 service-icon"> | ||||
|                 <ResolvedIcon icon={service.icon} /> | ||||
|               </div> | ||||
|             ))} | ||||
| @ -60,25 +62,25 @@ export default function Item({ service, group }) { | ||||
|               href={service.href} | ||||
|               target={service.target ?? settings.target ?? "_blank"} | ||||
|               rel="noreferrer" | ||||
|               className="flex-1 flex items-center justify-between rounded-r-md " | ||||
|               className="flex-1 flex items-center justify-between rounded-r-md service-title-text" | ||||
|             > | ||||
|               <div className="flex-1 px-2 py-2 text-sm text-left z-10"> | ||||
|               <div className="flex-1 px-2 py-2 text-sm text-left z-10 service-name"> | ||||
|                 {service.name} | ||||
|                 <p className="text-theme-500 dark:text-theme-300 text-xs font-light">{service.description}</p> | ||||
|                 <p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">{service.description}</p> | ||||
|               </div> | ||||
|             </a> | ||||
|           ) : ( | ||||
|             <div className="flex-1 flex items-center justify-between rounded-r-md "> | ||||
|               <div className="flex-1 px-2 py-2 text-sm text-left z-10"> | ||||
|             <div className="flex-1 flex items-center justify-between rounded-r-md service-title-text"> | ||||
|               <div className="flex-1 px-2 py-2 text-sm text-left z-10 service-name"> | ||||
|                 {service.name} | ||||
|                 <p className="text-theme-500 dark:text-theme-300 text-xs font-light">{service.description}</p> | ||||
|                 <p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">{service.description}</p> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           <div className="absolute top-0 right-0 w-1/2 flex flex-row justify-end gap-2 mr-2 z-30 pointer-events-none"> | ||||
|           <div className="absolute top-0 right-0 flex flex-row justify-end gap-2 mr-2 z-30 pointer-events-none service-tags"> | ||||
|               {service.ping && ( | ||||
|                 <div className="flex-shrink-0 flex items-center justify-center cursor-pointer"> | ||||
|                 <div className="flex-shrink-0 flex items-center justify-center service-tag service-ping"> | ||||
|                   <Ping group={group} service={service.name} /> | ||||
|                   <span className="sr-only">Ping status</span> | ||||
|                 </div> | ||||
| @ -88,7 +90,7 @@ export default function Item({ service, group }) { | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))} | ||||
|                   className="flex-shrink-0 flex items-center justify-center cursor-pointer" | ||||
|                   className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-container-stats" | ||||
|                 > | ||||
|                   <Status service={service} /> | ||||
|                   <span className="sr-only">View container stats</span> | ||||
| @ -98,7 +100,7 @@ export default function Item({ service, group }) { | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))} | ||||
|                   className="flex-shrink-0 flex items-center justify-center cursor-pointer" | ||||
|                   className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-app" | ||||
|                 > | ||||
|                   <KubernetesStatus service={service} /> | ||||
|                   <span className="sr-only">View container stats</span> | ||||
| @ -111,7 +113,7 @@ export default function Item({ service, group }) { | ||||
|           <div | ||||
|             className={classNames( | ||||
|               showStats || (statsOpen && !statsClosing) ? "max-h-[110px] opacity-100" : " max-h-[0] opacity-0", | ||||
|               "w-full overflow-hidden transition-all duration-300 ease-in-out" | ||||
|               "w-full overflow-hidden transition-all duration-300 ease-in-out service-stats" | ||||
|             )} | ||||
|           > | ||||
|             {(showStats || statsOpen) && <Docker service={{ widget: { container: service.container, server: service.server } }} />} | ||||
| @ -121,7 +123,7 @@ export default function Item({ service, group }) { | ||||
|           <div | ||||
|             className={classNames( | ||||
|               showStats || (statsOpen && !statsClosing) ? "max-h-[55px] opacity-100" : " max-h-[0] opacity-0", | ||||
|               "w-full overflow-hidden transition-all duration-300 ease-in-out" | ||||
|               "w-full overflow-hidden transition-all duration-300 ease-in-out service-stats" | ||||
|             )} | ||||
|           > | ||||
|             {(showStats || statsOpen) && <Kubernetes service={{ widget: { namespace: service.namespace, app: service.app, podSelector: service.podSelector } }} />} | ||||
|  | ||||
| @ -6,14 +6,14 @@ export default function KubernetesStatus({ service }) { | ||||
|   const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}?${podSelectorString}`); | ||||
| 
 | ||||
|   if (error) { | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.error")}> | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-error" title={t("docker.error")}> | ||||
|       <div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div> | ||||
|     </div> | ||||
|   } | ||||
| 
 | ||||
|   if (data && data.status === "running") { | ||||
|     return ( | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health ?? data.status}> | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status" title={data.health ?? data.status}> | ||||
|         <div className="text-[8px] font-bold text-emerald-500/80 uppercase">{data.health ?? data.status}</div> | ||||
|       </div> | ||||
|     ); | ||||
| @ -21,14 +21,14 @@ export default function KubernetesStatus({ service }) { | ||||
| 
 | ||||
|   if (data && (data.status === "not found" || data.status === "down" || data.status === "partial")) { | ||||
|     return ( | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}> | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-warning" title={data.status}> | ||||
|         <div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.status}</div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden"> | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-unknown"> | ||||
|       <div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -9,7 +9,7 @@ export default function List({ group, services, layout }) { | ||||
|     <ul | ||||
|       className={classNames( | ||||
|         layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col", | ||||
|         "mt-3" | ||||
|         "mt-3 services-list" | ||||
|       )} | ||||
|     > | ||||
|       {services.map((service) => ( | ||||
|  | ||||
| @ -9,7 +9,7 @@ export default function Ping({ group, service }) { | ||||
| 
 | ||||
|   if (error) { | ||||
|     return ( | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden"> | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-error"> | ||||
|         <div className="text-[8px] font-bold text-rose-500 uppercase">{t("ping.error")}</div> | ||||
|       </div> | ||||
|     ); | ||||
| @ -17,7 +17,7 @@ export default function Ping({ group, service }) { | ||||
|    | ||||
|   if (!data) { | ||||
|     return ( | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden"> | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-ping"> | ||||
|         <div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("ping.ping")}</div> | ||||
|       </div> | ||||
|     ); | ||||
| @ -27,14 +27,14 @@ export default function Ping({ group, service }) { | ||||
|    | ||||
|   if (data.status > 403) { | ||||
|     return ( | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}> | ||||
|       <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-status-invalid" title={statusText}> | ||||
|         <div className="text-[8px] font-bold text-rose-500/80">{data.status}</div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   return ( | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}> | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-status-valid" title={statusText}> | ||||
|       <div className="text-[8px] font-bold text-emerald-500/80">{t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 })}</div> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -7,7 +7,7 @@ export default function Status({ service }) { | ||||
|   const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`); | ||||
| 
 | ||||
|   if (error) { | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.error")}> | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-error" title={t("docker.error")}> | ||||
|       <div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div> | ||||
|     </div> | ||||
|   } | ||||
| @ -18,7 +18,7 @@ export default function Status({ service }) { | ||||
|     if (data.status?.includes("running")) { | ||||
|       if (data.health === "starting") { | ||||
|         return ( | ||||
|           <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.starting")}> | ||||
|           <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-starting" title={t("docker.starting")}> | ||||
|             <div className="text-[8px] font-bold text-blue-500/80 uppercase">{t("docker.starting")}</div> | ||||
|           </div> | ||||
|         ); | ||||
| @ -26,7 +26,7 @@ export default function Status({ service }) { | ||||
| 
 | ||||
|       if (data.health === "unhealthy") { | ||||
|         return ( | ||||
|           <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.unhealthy")}> | ||||
|           <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-unhealthy" title={t("docker.unhealthy")}> | ||||
|             <div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{t("docker.unhealthy")}</div> | ||||
|           </div> | ||||
|         ); | ||||
| @ -39,7 +39,7 @@ export default function Status({ service }) { | ||||
|       } | ||||
| 
 | ||||
|       return ( | ||||
|         <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusLabel}> | ||||
|         <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status" title={statusLabel}> | ||||
|           <div className="text-[8px] font-bold text-emerald-500/80 uppercase">{statusLabel}</div> | ||||
|         </div> | ||||
|       ); | ||||
| @ -50,7 +50,7 @@ export default function Status({ service }) { | ||||
|       else if (data.status === "exited") statusLabel = t("docker.exited") | ||||
|       else statusLabel = data.status.replace("partial", t("docker.partial")) | ||||
|       return ( | ||||
|         <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusLabel}> | ||||
|         <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status-warning" title={statusLabel}> | ||||
|           <div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{statusLabel}</div> | ||||
|         </div> | ||||
|       ); | ||||
| @ -58,7 +58,7 @@ export default function Status({ service }) { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden"> | ||||
|     <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status-unknown"> | ||||
|       <div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -17,7 +17,7 @@ export default function Widget({ service }) { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1"> | ||||
|     <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1 service-missing"> | ||||
|       <div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -8,7 +8,8 @@ export default function Block({ value, label }) { | ||||
|     <div | ||||
|       className={classNames( | ||||
|         "bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center text-center p-1", | ||||
|         value === undefined ? "animate-pulse" : "" | ||||
|         value === undefined ? "animate-pulse" : "", | ||||
|         "service-block" | ||||
|       )} | ||||
|     > | ||||
|       <div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div> | ||||
|  | ||||
| @ -36,5 +36,5 @@ export default function Container({ error = false, children, service }) { | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   return <div className="relative flex flex-row w-full">{visibleChildren}</div>; | ||||
|   return <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>; | ||||
| } | ||||
|  | ||||
| @ -38,7 +38,7 @@ export default function ColorToggle() { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-full self-center"> | ||||
|     <div id="color" className="w-full self-center"> | ||||
|       <Popover className="relative flex items-center"> | ||||
|         <Popover.Button className="outline-none"> | ||||
|           <IoColorPalette | ||||
|  | ||||
| @ -10,7 +10,7 @@ export default function Revalidate() { | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="rounded-full flex align-middle self-center mr-3"> | ||||
|     <div id="revalidate" className="rounded-full flex align-middle self-center mr-3"> | ||||
|       <MdRefresh onClick={() => revalidate()} className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" /> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -11,7 +11,7 @@ export default function ThemeToggle() { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="rounded-full flex self-end"> | ||||
|     <div id="theme" className="rounded-full flex self-end"> | ||||
|       <MdLightMode className="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5" /> | ||||
|       {theme === "dark" ? ( | ||||
|         <MdToggleOn | ||||
|  | ||||
| @ -25,7 +25,7 @@ export default function Version() { | ||||
|   const latestRelease = releaseData?.[0]; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex flex-row items-center"> | ||||
|     <div id="version" className="flex flex-row items-center"> | ||||
|       <span className="text-xs text-theme-500 dark:text-theme-400"> | ||||
|         {version === "main" || version === "dev" || version === "nightly" ? ( | ||||
|           <> | ||||
|  | ||||
| @ -30,7 +30,7 @@ export default function DateTime({ options }) { | ||||
|   }, [date, setDate, dateLocale, format]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Container options={options}> | ||||
|     <Container options={options} additionalClassNames="information-widget-datetime"> | ||||
|       <Raw> | ||||
|         <div className="flex flex-row items-center grow justify-end"> | ||||
|           <span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}> | ||||
|  | ||||
| @ -3,6 +3,7 @@ import { useContext } from "react"; | ||||
| import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa"; | ||||
| import { FiCpu, FiHardDrive } from "react-icons/fi"; | ||||
| import { useTranslation } from "next-i18next"; | ||||
| import classNames from "classnames"; | ||||
| 
 | ||||
| import Error from "../widget/error"; | ||||
| import Resource from "../widget/resource"; | ||||
| @ -32,7 +33,7 @@ export default function Widget({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Resources options={options}> | ||||
|     return <Resources options={options} additionalClassNames="information-widget-glances"> | ||||
|       { options.cpu !== false && <Resource icon={FiCpu} label={t("glances.wait")} percentage="0" /> } | ||||
|       { options.mem !== false && <Resource icon={FaMemory} label={t("glances.wait")} percentage="0" /> } | ||||
|       { options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" /> } | ||||
| @ -69,8 +70,10 @@ export default function Widget({ options }) { | ||||
|       : [data.fs.find((d) => d.mnt_point === options.disk)].filter((d) => d); | ||||
|   } | ||||
| 
 | ||||
|   const addedClasses = classNames('information-widget-glances', { 'expanded': options.expanded }) | ||||
| 
 | ||||
|   return ( | ||||
|     <Resources options={options} target={settings.target ?? "_blank"}> | ||||
|     <Resources options={options} target={settings.target ?? "_blank"} additionalClassNames={addedClasses}> | ||||
|       {options.cpu !== false && <Resource | ||||
|         icon={FiCpu} | ||||
|         value={t("common.number", { | ||||
|  | ||||
| @ -14,7 +14,7 @@ const textSizes = { | ||||
| 
 | ||||
| export default function Greeting({ options }) { | ||||
|   if (options.text) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-greeting"> | ||||
|       <Raw> | ||||
|         <span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}> | ||||
|           {options.text} | ||||
|  | ||||
| @ -36,7 +36,7 @@ export default function Widget({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-kubernetes"> | ||||
|       <Raw> | ||||
|         <div className="flex flex-row self-center flex-wrap justify-between"> | ||||
|           {cluster.show && | ||||
| @ -50,7 +50,7 @@ export default function Widget({ options }) { | ||||
|     </Container>; | ||||
|   } | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-kubernetes"> | ||||
|     <Raw> | ||||
|       <div className="flex flex-row self-center flex-wrap justify-between"> | ||||
|         {cluster.show && | ||||
|  | ||||
| @ -5,14 +5,14 @@ import ResolvedIcon from "components/resolvedicon" | ||||
| 
 | ||||
| export default function Logo({ options }) { | ||||
|   return ( | ||||
|     <Container options={options}> | ||||
|     <Container options={options} additionalClassNames={`information-widget-logo ${  options.icon ? 'resolved' : 'fallback'}`}> | ||||
|       <Raw> | ||||
|         {options.icon ? | ||||
|         <div className="mr-3"> | ||||
|         <div className="resolved mr-3"> | ||||
|           <ResolvedIcon icon={options.icon} width={48} height={48} /> | ||||
|         </div> : | ||||
|         // fallback to homepage logo | ||||
|         <div className="w-12 h-12"> | ||||
|         <div className="fallback w-12 h-12"> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             viewBox="0 0 1024 1024" | ||||
|  | ||||
| @ -17,14 +17,14 @@ export default function Longhorn({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="infomation-widget-longhorn"> | ||||
|       <Raw> | ||||
|         <div className="flex flex-row self-center flex-wrap justify-between" /> | ||||
|       </Raw> | ||||
|     </Container>; | ||||
|   } | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="infomation-widget-longhorn"> | ||||
|     <Raw> | ||||
|       <div className="flex flex-row self-center flex-wrap justify-between"> | ||||
|         {data.nodes | ||||
|  | ||||
| @ -8,6 +8,7 @@ export default function Node({ data, expanded, labels }) { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   return <Resource | ||||
|     additionalClassNames="information-widget-longhorn-node" | ||||
|     icon={FaThermometerHalf} | ||||
|     value={t("common.bytes", { value: data.node.available })} | ||||
|     label={t("resources.free")} | ||||
|  | ||||
| @ -24,7 +24,7 @@ function Widget({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-openmeteo"> | ||||
|       <PrimaryText>{t("weather.updating")}</PrimaryText> | ||||
|       <SecondaryText>{t("weather.wait")}</SecondaryText> | ||||
|       <WidgetIcon icon={WiCloudDown} size="l" /> | ||||
| @ -35,7 +35,7 @@ function Widget({ options }) { | ||||
|   const condition = data.current_weather.weathercode; | ||||
|   const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night"; | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-openmeteo"> | ||||
|     <PrimaryText> | ||||
|       {options.label && `${options.label}, `} | ||||
|       {t("common.number", { | ||||
| @ -81,7 +81,7 @@ export default function OpenMeteo({ options }) { | ||||
|   // if (!requesting && !location) requestLocation(); | ||||
| 
 | ||||
|   if (!location) { | ||||
|     return <ContainerButton options={options} callback={requestLocation} > | ||||
|     return <ContainerButton options={options} callback={requestLocation} additionalClassNames="information-widget-openmeteo-location-button"> | ||||
|       <PrimaryText>{t("weather.current")}</PrimaryText> | ||||
|       <SecondaryText>{t("weather.allow")}</SecondaryText> | ||||
|       <WidgetIcon icon={ requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse /> | ||||
|  | ||||
| @ -24,7 +24,7 @@ function Widget({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-openweathermap"> | ||||
|       <PrimaryText>{t("weather.updating")}</PrimaryText> | ||||
|       <SecondaryText>{t("weather.wait")}</SecondaryText> | ||||
|       <WidgetIcon icon={WiCloudDown} size="l" /> | ||||
| @ -36,7 +36,7 @@ function Widget({ options }) { | ||||
|   const condition = data.weather[0].id; | ||||
|   const timeOfDay = data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night"; | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-openweathermap"> | ||||
|     <PrimaryText>{options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })}</PrimaryText> | ||||
|     <SecondaryText>{data.weather[0].description}</SecondaryText> | ||||
|     <WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" /> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| export default function UsageBar({ percent }) { | ||||
| export default function UsageBar({ percent, additionalClassNames='' }) { | ||||
|   return ( | ||||
|     <div className="mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20"> | ||||
|     <div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}> | ||||
|       <div | ||||
|         className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000" | ||||
|         style={{ | ||||
|  | ||||
| @ -103,7 +103,7 @@ export default function Search({ options }) { | ||||
|     localStorage.setItem(localStorageKey, provider.name); | ||||
|   } | ||||
| 
 | ||||
|   return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow" > | ||||
|   return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow information-widget-search" > | ||||
|     <Raw> | ||||
|       <div className="flex-col relative h-8 my-4 min-w-fit"> | ||||
|         <div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" /> | ||||
|  | ||||
| @ -25,7 +25,7 @@ export default function Widget({ options }) { | ||||
|   const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default"); | ||||
| 
 | ||||
|   if (!defaultSite) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-unifi-console"> | ||||
|       <PrimaryText>{t("unifi.wait")}</PrimaryText> | ||||
|       <WidgetIcon icon={SiUbiquiti} /> | ||||
|     </Container>; | ||||
| @ -43,7 +43,7 @@ export default function Widget({ options }) { | ||||
| 
 | ||||
|   const dataEmpty = !(wan.show || lan.show || wlan.show || uptime); | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-unifi-console"> | ||||
|     <Raw> | ||||
|       <div className="flex-none flex flex-row items-center mr-3 py-1.5"> | ||||
|       <div className="flex flex-col"> | ||||
|  | ||||
| @ -24,7 +24,7 @@ function Widget({ options }) { | ||||
|   } | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <Container options={options}> | ||||
|     return <Container options={options} additionalClassNames="information-widget-weather"> | ||||
|       <PrimaryText>{t("weather.updating")}</PrimaryText> | ||||
|       <SecondaryText>{t("weather.wait")}</SecondaryText> | ||||
|       <WidgetIcon icon={WiCloudDown} size="l" /> | ||||
| @ -35,7 +35,7 @@ function Widget({ options }) { | ||||
|   const condition = data.current.condition.code; | ||||
|   const timeOfDay = data.current.is_day ? "day" : "night"; | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-weather"> | ||||
|     <PrimaryText> | ||||
|       {options.label && `${options.label}, `} | ||||
|       {t("common.number", { | ||||
|  | ||||
| @ -35,9 +35,9 @@ export function getAllClasses(options, additionalClassNames = '') { | ||||
| 
 | ||||
| export function getInnerBlock(children) { | ||||
|   // children won't be an array if it's Raw component | ||||
|   return Array.isArray(children) && <div className="flex flex-row items-center justify-end"> | ||||
|     <div className="flex flex-col items-center">{children.find(child => child.type === WidgetIcon)}</div> | ||||
|     <div className="flex flex-col ml-3 text-left"> | ||||
|   return Array.isArray(children) && <div className="flex flex-row items-center justify-end widget-inner"> | ||||
|     <div className="flex flex-col items-center widget-inner-icon">{children.find(child => child.type === WidgetIcon)}</div> | ||||
|     <div className="flex flex-col ml-3 text-left widget-inner-text"> | ||||
|       {children.find(child => child.type === PrimaryText)} | ||||
|       {children.find(child => child.type === SecondaryText)} | ||||
|     </div> | ||||
| @ -54,7 +54,7 @@ export function getBottomBlock(children) { | ||||
| 
 | ||||
| export default function Container({ children = [], options, additionalClassNames = '' }) { | ||||
|   return ( | ||||
|     <div className={getAllClasses(options, additionalClassNames)}> | ||||
|     <div className={getAllClasses(options, `${ additionalClassNames } widget-container`)}> | ||||
|       {getInnerBlock(children)} | ||||
|       {getBottomBlock(children)} | ||||
|     </div> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; | ||||
| 
 | ||||
| export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) { | ||||
|   return ( | ||||
|     <button type="button" onClick={callback} className={getAllClasses(options, additionalClassNames)}> | ||||
|     <button type="button" onClick={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-container-button`}> | ||||
|       {getInnerBlock(children)} | ||||
|       {getBottomBlock(children)} | ||||
|     </button> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; | ||||
| 
 | ||||
| export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) { | ||||
|   return ( | ||||
|     <form type="button" onSubmit={callback} className={getAllClasses(options, additionalClassNames)}> | ||||
|     <form type="button" onSubmit={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-form`}> | ||||
|       {getInnerBlock(children)} | ||||
|       {getBottomBlock(children)} | ||||
|     </form> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container"; | ||||
| 
 | ||||
| export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) { | ||||
|   return ( | ||||
|     <a href={options.url} target={target} className={getAllClasses(options, additionalClassNames)}> | ||||
|     <a href={options.url} target={target} className={`${ getAllClasses(options, additionalClassNames) } information-widget-link`}> | ||||
|       {getInnerBlock(children)} | ||||
|       {getBottomBlock(children)} | ||||
|     </a> | ||||
|  | ||||
| @ -8,7 +8,7 @@ import WidgetIcon from "./widget_icon"; | ||||
| export default function Error({ options }) { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   return <Container options={options}> | ||||
|   return <Container options={options} additionalClassNames="information-widget-error"> | ||||
|     <PrimaryText>{t("widget.api_error")}</PrimaryText> | ||||
|     <WidgetIcon icon={BiError} size="l" /> | ||||
|   </Container>; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| export default function PrimaryText({ children }) { | ||||
|   return ( | ||||
|     <span className="text-theme-800 dark:text-theme-200 text-sm">{children}</span> | ||||
|     <span className="primary-text text-theme-800 dark:text-theme-200 text-sm">{children}</span> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import UsageBar from "../resources/usage-bar"; | ||||
| 
 | ||||
| export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) { | ||||
| export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false, additionalClassNames='' }) { | ||||
|   const Icon = icon; | ||||
| 
 | ||||
|   return <div className="flex-none flex flex-row items-center mr-3 py-1.5"> | ||||
|     <Icon className="text-theme-800 dark:text-theme-200 w-5 h-5"/> | ||||
|     <div className="flex flex-col ml-3 text-left min-w-[85px]"> | ||||
|   return <div className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${ additionalClassNames }`}> | ||||
|     <Icon className="text-theme-800 dark:text-theme-200 w-5 h-5 resource-icon"/> | ||||
|     <div className={ `flex flex-col ml-3 text-left min-w-[85px] ${ expanded ? ' expanded' : ''}`}> | ||||
|       <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between"> | ||||
|         <div className="pl-0.5">{value}</div> | ||||
|         <div className="pr-1">{label}</div> | ||||
| @ -15,7 +15,7 @@ export default function Resource({ children, icon, value, label, expandedValue = | ||||
|           <div className="pr-1">{expandedLabel}</div> | ||||
|         </div> | ||||
|       } | ||||
|       { percentage >= 0 && <UsageBar percent={percentage} /> } | ||||
|       { percentage >= 0 && <UsageBar percent={percentage} additionalClassNames="resource-usage" /> } | ||||
|       { children } | ||||
|     </div> | ||||
|   </div>; | ||||
|  | ||||
| @ -1,12 +1,15 @@ | ||||
| import classNames from "classnames"; | ||||
| 
 | ||||
| import ContainerLink from "./container_link"; | ||||
| import Resource from "./resource"; | ||||
| import Raw from "./raw"; | ||||
| import WidgetLabel from "./widget_label"; | ||||
| 
 | ||||
| export default function Resources({ options, children, target }) { | ||||
| export default function Resources({ options, children, target, additionalClassNames }) { | ||||
|   const widgetParts = [].concat(...children); | ||||
|   const addedClassNames = classNames('information-widget-resources', additionalClassNames); | ||||
| 
 | ||||
|   return <ContainerLink options={options} target={target}> | ||||
|   return <ContainerLink options={options} target={target} additionalClassNames={ addedClassNames }> | ||||
|     <Raw> | ||||
|       <div className="flex flex-row self-center flex-wrap justify-between"> | ||||
|         { widgetParts.filter(child => child && child.type === Resource) } | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| export default function SecondaryText({ children }) { | ||||
|   return ( | ||||
|     <span className="text-theme-800 dark:text-theme-200 text-xs">{children}</span> | ||||
|     <span className="secondary-text text-theme-800 dark:text-theme-200 text-xs">{children}</span> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| export default function WidgetIcon({ icon, size = "s", pulse = false }) { | ||||
|   const Icon = icon; | ||||
|   let additionalClasses = "text-theme-800 dark:text-theme-200 "; | ||||
|   let additionalClasses = "information-widget-icon text-theme-800 dark:text-theme-200 "; | ||||
| 
 | ||||
|   switch (size) { | ||||
|     case "m": additionalClasses += "w-6 h-6 "; break; | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| export default function WidgetLabel({ label = "" }) { | ||||
|   return <div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div> | ||||
|   return <div className="information-widget-label pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div> | ||||
| } | ||||
|  | ||||
							
								
								
									
										35
									
								
								src/pages/api/config/[path].js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/pages/api/config/[path].js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import path from "path"; | ||||
| import fs from "fs"; | ||||
| 
 | ||||
| import { CONF_DIR } from "utils/config/config"; | ||||
| import createLogger from "utils/logger"; | ||||
| 
 | ||||
| const logger = createLogger("configFileService"); | ||||
| 
 | ||||
| /** | ||||
|  * @param {import("next").NextApiRequest} req | ||||
|  * @param {import("next").NextApiResponse} res | ||||
|  */ | ||||
| export default async function handler(req, res) { | ||||
|   const { path: relativePath } = req.query; | ||||
| 
 | ||||
|   // only two supported files, for now
 | ||||
|   if (!['custom.css', 'custom.js'].includes(relativePath)) | ||||
|   { | ||||
|     return res.status(422).end('Unsupported file'); | ||||
|   } | ||||
| 
 | ||||
|   const filePath = path.join(CONF_DIR, relativePath); | ||||
| 
 | ||||
|   try { | ||||
|       // Read the content of the file or return empty content
 | ||||
|       const fileContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''; | ||||
|       // hard-coded since we only support two known files for now
 | ||||
|       const mimeType = (relativePath === 'custom.css') ? 'text/css' : 'text/javascript'; | ||||
|       res.setHeader('Content-Type', mimeType); | ||||
|       return res.status(200).send(fileContent); | ||||
|     } catch (error) { | ||||
|       logger.error(error); | ||||
|       return res.status(500).end('Internal Server Error'); | ||||
|     } | ||||
| } | ||||
| @ -4,7 +4,7 @@ import { readFileSync } from "fs"; | ||||
| 
 | ||||
| import checkAndCopyConfig, { CONF_DIR } from "utils/config/config"; | ||||
| 
 | ||||
| const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml"]; | ||||
| const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml", "custom.css", "custom.js"]; | ||||
| 
 | ||||
| function hash(buffer) { | ||||
|   const hashSum = createHash("sha256"); | ||||
|  | ||||
| @ -8,6 +8,7 @@ import { useEffect, useContext, useState, useMemo } from "react"; | ||||
| import { BiError } from "react-icons/bi"; | ||||
| import { serverSideTranslations } from "next-i18next/serverSideTranslations"; | ||||
| 
 | ||||
| import FileContent from "components/filecontent"; | ||||
| import ServicesGroup from "components/services/group"; | ||||
| import BookmarksGroup from "components/bookmarks/group"; | ||||
| import Widget from "components/widgets/widget"; | ||||
| @ -239,7 +240,7 @@ function Home({ initialSettings }) { | ||||
|     const bookmarkGroups = bookmarks.filter(group => settings.layout?.[group.name] === undefined); | ||||
| 
 | ||||
|     return <> | ||||
|       {layoutGroups.length > 0 && <div key="layoutGroups" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|       {layoutGroups.length > 0 && <div key="layoutGroups" id="layout-groups" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|         {layoutGroups.map((group) => ( | ||||
|           group.services ?  | ||||
|             (<ServicesGroup | ||||
| @ -259,7 +260,7 @@ function Home({ initialSettings }) { | ||||
|         ) | ||||
|       )} | ||||
|       </div>} | ||||
|       {serviceGroups?.length > 0 && <div key="services" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|       {serviceGroups?.length > 0 && <div key="services" id="services" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|         {serviceGroups.map((group) => ( | ||||
|           <ServicesGroup | ||||
|             key={group.name} | ||||
| @ -271,7 +272,7 @@ function Home({ initialSettings }) { | ||||
|           /> | ||||
|         ))} | ||||
|       </div>} | ||||
|       {bookmarkGroups?.length > 0 && <div key="bookmarks" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|       {bookmarkGroups?.length > 0 && <div key="bookmarks" id="bookmarks" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2"> | ||||
|         {bookmarkGroups.map((group) => ( | ||||
|           <BookmarksGroup | ||||
|             key={group.name} | ||||
| @ -311,6 +312,24 @@ function Home({ initialSettings }) { | ||||
|         <meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} /> | ||||
|         <meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} /> | ||||
|       </Head> | ||||
| 
 | ||||
|       <link rel="preload" href="/api/config/custom.css" as="fetch" crossorigin="anonymous" /> | ||||
|       <style data-name="custom.css"> | ||||
|         <FileContent path="custom.css" | ||||
|           loadingValue="/* Loading custom CSS... */" | ||||
|           errorValue="/* Failed to load custom CSS... */" | ||||
|           emptyValue="/* No custom CSS */" | ||||
|         /> | ||||
|       </style> | ||||
|       <link rel="preload" href="/api/config/custom.js" as="fetch" crossorigin="anonymous" /> | ||||
|       <script data-name="custom.js"> | ||||
|         <FileContent path="custom.js" | ||||
|           loadingValue="/* Loading custom JS... */" | ||||
|           errorValue="/* Failed to load custom JS... */" | ||||
|           emptyValue="/* No custom JS */" | ||||
|         /> | ||||
|       </script> | ||||
| 
 | ||||
|       <div className="relative container m-auto flex flex-col justify-start z-10 h-full"> | ||||
|         <QuickLaunch | ||||
|           servicesAndBookmarks={servicesAndBookmarks} | ||||
| @ -321,6 +340,7 @@ function Home({ initialSettings }) { | ||||
|           searchProvider={settings.quicklaunch?.hideInternetSearch ? null : searchProvider} | ||||
|         /> | ||||
|         <div | ||||
|           id="information-widgets" | ||||
|           className={classNames( | ||||
|             "flex flex-row flex-wrap justify-between", | ||||
|             headerStyles[headerStyle], | ||||
| @ -335,7 +355,7 @@ function Home({ initialSettings }) { | ||||
|                   <Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false, cardBlur: settings.cardBlur }} /> | ||||
|                 ))} | ||||
| 
 | ||||
|               <div className={classNames( | ||||
|               <div id="information-widgets-right" className={classNames( | ||||
|                 "m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end", | ||||
|                 headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2" | ||||
|               )}> | ||||
| @ -351,14 +371,14 @@ function Home({ initialSettings }) { | ||||
| 
 | ||||
|         {servicesAndBookmarksGroups} | ||||
| 
 | ||||
|         <div className="flex flex-col mt-auto p-8 w-full"> | ||||
|           <div className="flex w-full justify-end"> | ||||
|         <div id="footer" className="flex flex-col mt-auto p-8 w-full"> | ||||
|           <div id="style" className="flex w-full justify-end"> | ||||
|             {!settings?.color && <ColorToggle />} | ||||
|             <Revalidate /> | ||||
|             {!settings.theme && <ThemeToggle />} | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex mt-4 w-full justify-end"> | ||||
|           <div id="version" className="flex mt-4 w-full justify-end"> | ||||
|             {!settings.hideVersion && <Version />} | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
							
								
								
									
										0
									
								
								src/skeleton/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/skeleton/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/skeleton/custom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/skeleton/custom.js
									
									
									
									
									
										Normal file
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user