import { Badge } from "../../tremor/Badge"; import { Label, Metric } from "../../tremor/Text"; import { Button } from "../../tremor/Button"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useDashboard } from "../../layouts/Dashboard/useDashboard"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { ArrowDownTrayIcon, ArrowLeftIcon, ArrowPathIcon, ArrowRightIcon, Bars3Icon, ClockIcon, DocumentChartBarIcon, FunnelIcon, PencilIcon, PhotoIcon, PlusIcon, PresentationChartBarIcon, TrashIcon, } from "@heroicons/react/24/outline"; import { toast } from "sonner"; import { useBackend } from "../../layouts/Wrapper"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuIconWrapper, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../tremor/DropdownMenu"; import { useTheme } from "../../layouts/Dashboard/useTheme"; import { Popover, PopoverContent, PopoverTrigger } from "../../tremor/Popover"; import { FilterBar } from "../FilterBar"; import { useAutomationsModal } from "../AutomationsModal/useAutomationsModal"; import { Tooltip } from "../../tremor/Tooltip"; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { Text } from "../../tremor/Text"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/20/solid"; import { Icon } from "../../tremor/Icon"; dayjs.extend(relativeTime); function formatList(items: string[]) { if (items.length === 0) return <>; if (items.length === 1) return {items[0]}; if (items.length === 2) return <>{items[0]} and {items[1]}; return <>{items.slice(0, -1).map(a => {a})}and {items[items.length - 1]}; } export const LastUpdatedBadge: React.FC<{}> = ({ }) => { const { dashboard, refreshing } = useDashboard(); const [key, setKey] = useState(new Date().toISOString()); const [lastUpdated, setLastUpdated] = useState(dashboard?.last_updated_at || dashboard?.created_at); useEffect(() => { let interval = setInterval(() => { setKey(new Date().toISOString()); }, 1000); return () => { clearInterval(interval); }; }, []); useEffect(() => { if (dashboard) { setLastUpdated(dashboard.last_updated_at || dashboard.created_at); } }, [dashboard]); if (refreshing) { return ( Auto refreshing... ); } return ( {dayjs(lastUpdated).fromNow()} ); }; export const DashboardHeaderTab: React.FC<{ item: { label: string, id: number } }> = ({ item }) => { const [editing, setEditing] = useState(false); const [title, setTitle] = useState(item.label); const componentRef = useRef(null); const { backend, adminMode } = useBackend(); const { dashboard, setDashboard, widgets } = useDashboard(); const { containerRef } = useBackend(); const updateDashboard = () => { setEditing(false); if (dashboard) { const newTabs = dashboard.tabs.map((tab) => { if (tab.id === item.id) { return { ...tab, label: title, } } return tab; }); const newDashboard = { ...dashboard, tabs: newTabs, }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard); } } useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (componentRef.current && !componentRef.current.contains(event.target as Node)) { updateDashboard(); } }; if (editing) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [editing]); const moveTab = (position: "left" | "right") => { if (dashboard) { const currentIndex = dashboard.tabs.findIndex(tab => tab.id === item.id); if (currentIndex === -1) return; const newTabs = [...dashboard.tabs]; let newIndex; if (position === "left") { newIndex = Math.max(0, currentIndex - 1); } else { newIndex = Math.min(newTabs.length - 1, currentIndex + 1); } // Only move if the new position is different if (newIndex !== currentIndex) { // Remove the tab from its current position const [movedTab] = newTabs.splice(currentIndex, 1); // Insert it at the new position newTabs.splice(newIndex, 0, movedTab); const newDashboard = { ...dashboard, tabs: newTabs, }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard); } } } const deleteTab = () => { if (dashboard) { const widgetsExist = widgets?.some(widget => widget.tab === item.id); if (widgetsExist) { toast.error("You cannot delete a tab with widgets, delete all the widgets in the tab and try again"); return; } const newTabs = dashboard.tabs.filter((tab) => tab.id !== item.id); const newDashboard = { ...dashboard, tabs: newTabs, }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard); } } const editable = adminMode || dashboard?.settings?.can_edit_tabs; return (
{ if (editable) { setEditing(true); } }} > {editing ? { if (e.key === "Enter") { updateDashboard(); } }} onChange={(e) => { setTitle(e.target.value); }} /> :
{item.label} {editable && (
setEditing(true)}> Rename moveTab("left")}> Move Left moveTab("right")}> Move Right deleteTab()}> Delete
)}
}
) } export const DashboardHeader: React.FC<{ children?: React.ReactNode; className?: string; variant?: "default" | "pdf" | "pptx" }> = ({ children, className, variant }) => { const headerRef = useRef(null); const [headerHeight, setHeaderHeight] = useState(0); // Effect to measure and update header height useEffect(() => { if (headerRef.current) { const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { setHeaderHeight(entry.contentRect.height); } }); resizeObserver.observe(headerRef.current); return () => resizeObserver.disconnect(); } }, []); const { setOpen } = useAutomationsModal(); const { dashboard, tab, setTab, progress, refreshing, setDashboard } = useDashboard(); const theme = useTheme(); const { backend, adminMode, containerRef } = useBackend(); const exportDashboard = useCallback( (format: "csv" | "xlsx" | "pdf" | "png" | "jpeg" | "pptx") => { if (!backend || !dashboard) { return; } toast.promise( async () => { return backend.dashboard(dashboard.id).export(format, theme, tab, (event) => { // console.log(event); }); }, { loading: `Exporting dashboard as ${format}...`, success: async (output: any) => { try { // Get the file URL, replacing host.docker.internal with localhost if needed const fileUrl = output.url.replace("host.docker.internal", "localhost"); // Create a link element const link = document.createElement('a'); link.href = fileUrl; link.download = output.fileName || `export.${format}`; // Set the filename link.style.display = 'none'; // Add to document, click and remove document.body.appendChild(link); link.click(); // Clean up setTimeout(() => { document.body.removeChild(link); }, 100); } catch (error) { console.error('Download failed:', error); toast.error('Download failed. Please try again.'); } return "Dashboard exported successfully"; }, error: (e: any) => { return "Failed to export dashboard: " + e.message; }, } ); }, [dashboard, backend, tab, theme] ); const ImageDownloadEnabled = useMemo(() => { if (dashboard?.settings && dashboard.settings.disable_download_images) return false; return true; }, [dashboard]); const DocumentDownloadEnabled = useMemo(() => { if (dashboard?.settings && dashboard.settings.disable_download_documents) return false; return true; }, [dashboard]); const ReportDownloadEnabled = useMemo(() => { if (dashboard?.settings && dashboard.settings.disable_download_reports) return false; return true; }, [dashboard]); const filtered = useMemo(() => { let filters = dashboard?.filters || []; if (filters.length === 0) return []; let results = [] as string[]; for (let filter of filters) { if (filter.type === "text" && filter.options.length !== filter.values.length) { results.push(filter.label); } if (filter.type === "number" && (filter.options[0] !== filter.values[0] || filter.options[1] !== filter.values[1])) { results.push(filter.label); } if (filter.type === "date" && (filter.options[0] !== filter.values[0] || filter.options[1] !== filter.values[1])) { results.push(filter.label); } } return results; }, [dashboard]) const border_radius = useMemo(() => { return dashboard?.settings?.widget_border_radius === undefined ? 16 : dashboard?.settings?.widget_border_radius; }, [dashboard]); const padding = useMemo(() => { return dashboard?.settings?.widget_padding === undefined ? 10 : dashboard?.settings?.widget_padding; }, [dashboard]); if (dashboard?.settings?.hide_header && !adminMode) return <>; const DownloadDropdown = (
{ReportDownloadEnabled && ( <> Report formats exportDashboard("xlsx")}> Excel exportDashboard("csv")}> CSV )} {DocumentDownloadEnabled && ReportDownloadEnabled && ( )} {DocumentDownloadEnabled && ( <> Document formats exportDashboard("pdf")}> PDF exportDashboard("pptx")}> Powerpoint )} {ImageDownloadEnabled && (DocumentDownloadEnabled || ReportDownloadEnabled) && ( )} {ImageDownloadEnabled && ( <> Image formats exportDashboard("png")}> PNG exportDashboard("jpeg")}> JPEG )}
); if (variant === "pdf" || variant === "pptx") { return ( <>
{(dashboard?.tabs || []).filter(a => !a.hidden).length > 1 ? (
{(dashboard?.tabs || []).filter(a => !a.hidden).map(a => ( ))}
) :
} {(ImageDownloadEnabled || ReportDownloadEnabled || DocumentDownloadEnabled) && DownloadDropdown }
) } const reorder = (list: any[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; const onDragEnd = (result: any) => { if (!result.destination || !dashboard) { return; } const items = reorder( dashboard?.tabs || [], result.source.index, result.destination.index ); const updatedDashboard = { ...dashboard, tabs: items }; setDashboard(updatedDashboard); backend?.dashboards.update(dashboard.id, updatedDashboard) }; const addTab = () => { if (!dashboard) return; if (!dashboard.tabs || dashboard.tabs.length === 0) { const newTabs = [{ id: 0, label: "New tab" }]; const newDashboard = { ...dashboard, tabs: newTabs }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard) } else { const maxTab = Math.max(...dashboard.tabs.map(a => a.id), 0) + 1; const newTabs = [...dashboard.tabs, { id: maxTab, label: "New tab" }]; const newDashboard = { ...dashboard, tabs: newTabs }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard) } } const hideTab = (tabId: number, hidden: boolean) => { if (!dashboard) return; const newTabs = dashboard.tabs.map(a => { if (a.id === tabId) { return { ...a, hidden } } return a; }); const newDashboard = { ...dashboard, tabs: newTabs }; setDashboard(newDashboard); backend?.dashboards.update(dashboard.id, newDashboard) } const editable = adminMode || dashboard?.settings?.can_edit_tabs; return ( <>
{dashboard && dashboard.thumbnail && dashboard.thumbnail.trim() !== "" && ( {dashboard.title})}
{dashboard ? ( <> {dashboard?.title || ""} ) : (
)}
{dashboard?.settings?.filters && dashboard.filters.length > 0 && ( e.preventDefault()} container={containerRef?.current} className="!onvo-p-0 onvo-z-50 onvo-w-[320px] onvo-backdrop-blur-lg !onvo-bg-transparent">
)} {(dashboard?.settings?.can_schedule_reports) && ( )} {(ImageDownloadEnabled || ReportDownloadEnabled || DocumentDownloadEnabled) && DownloadDropdown } {children}
{refreshing &&
} {dashboard?.settings?.enable_tabs && (
{(provided, snapshot) => (
{(dashboard?.tabs || []).filter(a => !a.hidden).map((item, index) => ( {(provided, snapshot) => (
{ setTab(item.id); }}>
)}
))} {provided.placeholder}
)}
{editable && ( { addTab(); }} /> )}
{(dashboard?.tabs || []).map(a => ( { setTab(a.id); }}> {a.label}
{a.hidden ? { if (editable) { hideTab(a.id, false); } }} className="onvo-size-4" /> : { if (editable) { hideTab(a.id, true); } }} className="onvo-size-4" />}
))}
)}
); };