import React, { useEffect, useMemo, useRef, useState } from "react";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
import { toast } from "sonner";
import { LogType, Widget } from "@onvo-ai/js";
import {
ArrowsRightLeftIcon,
DocumentChartBarIcon,
DocumentDuplicateIcon,
FunnelIcon,
PencilSquareIcon,
PhotoIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import ChartBase from "./ChartBase";
import { Card } from "../../tremor/Card";
import { Icon } from "../../tremor/Icon";
import { useDashboard } from "../../layouts/Dashboard/useDashboard";
import { useBackend } from "../../layouts/Wrapper";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuIconWrapper,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSubMenu,
DropdownMenuSubMenuContent,
DropdownMenuSubMenuTrigger,
DropdownMenuTrigger,
} from "../../tremor/DropdownMenu";
import { useTextWidgetModal } from "../TextWidgetModal";
import { useImageWidgetModal } from "../ImageWidgetModal";
import { useEditWidgetModal } from "../EditWidgetModal";
import { useTheme } from "../../layouts/Dashboard/useTheme";
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
import { SortDirection } from "react-data-grid";
import { usePagination } from "./usePagination";
import { PaginationComponent } from "./PaginationComponent";
import { ChartPlaceholder } from "../ChartLoader";
import { useDrilldownModal } from "../DrilldownModal";
const DragHandle = () => {
return (
);
};
const ChartCard: React.FC<{
widget: Widget;
className?: string;
hideOptions?: boolean;
footer?: React.ReactNode;
}> = ({ widget, className, footer, hideOptions }) => {
const { dashboard, refreshWidgets, tab, queue } = useDashboard();
const { setOpen: setEditModalOpen } = useEditWidgetModal();
const { setOpen: setTextModalOpen } = useTextWidgetModal();
const { setOpen: setImageModalOpen } = useImageWidgetModal();
const { setOpen: setDrilldownModalOpen } = useDrilldownModal();
const { backend, adminMode, containerRef } = useBackend();
const theme = useTheme();
const cardRef = useRef(null);
const [cache, setCache] = useState(null);
// Use pagination store
const {
pagination,
setStats,
stats,
setPagination,
resetPagination
} = usePagination();
const duplicate = async () => {
let newObj: any = { ...widget };
delete newObj.id;
if (!backend) return;
toast.promise(
async () => {
let wid = await backend.widgets.create(newObj);
let data = await backend.widget(wid.id).updateCache();
return wid;
},
{
loading: "Duplicating widget...",
success: (newWidget) => {
refreshWidgets(backend);
backend.logs.create({
type: LogType.CreateWidget,
dashboard: widget.dashboard,
widget: newWidget.id,
})
return "Widget duplicated";
},
error: (error) => "Error duplicating widget: " + error.message,
}
);
};
const moveTab = async (newTab: number) => {
let newObj: any = { ...widget, tab: newTab };
if (!backend) return;
toast.promise(
() => {
return backend.widgets.update(widget.id, newObj) as Promise;
},
{
loading: "Moving widget...",
success: (newWidget) => {
refreshWidgets(backend);
return "Widget moved";
},
error: (error) => "Error moving widget: " + error.message,
}
);
};
const deleteWidget = async () => {
if (!backend) return;
toast.promise(
() => {
return backend.widgets.delete(widget.id);
},
{
loading: "Deleting widget...",
success: () => {
backend.logs.create({
type: LogType.DeleteWidget,
dashboard: widget.dashboard
});
refreshWidgets(backend);
return "Widget deleted";
},
error: (error) => "Error deleting widget: " + error.message,
}
);
};
const exportWidget = (format: "svg" | "png" | "csv" | "xlsx" | "jpeg") => {
if (!backend) return;
toast.promise(
() => {
return backend.widget(widget.id).export(format, theme, (event) => {
console.log("EVENT: ", event);
});
},
{
loading: `Exporting widget 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 "Widget exported";
},
error: (error) => "Error exporting widget: " + error.message,
}
);
};
// Function to fetch paginated data from server
const fetchPaginatedData = async (page: number, size: number, column?: string, direction?: SortDirection) => {
if (!backend || !widget || !widget.code || widget.code.trim() === "") return;
try {
// Fetch paginated data from server
const { data, pagination: paginationData } = await queue.add(() => backend.widget(widget.id).cache({
page,
pageSize: size,
sortField: column,
sortDirection: direction === "DESC" ? "desc" : "asc"
}));
setCache(data);
// Update cache with paginated data
if (paginationData) {
// Update pagination metadata
setStats({
totalRows: paginationData.totalRows || 0,
totalPages: paginationData.totalPages || 1
});
}
} catch (error) {
console.error("Error fetching paginated data:", error);
}
};
// Handle sort change from pagination store
const updatePagination = (newPagination: any) => {
if (!newPagination) return;
fetchPaginatedData(pagination.currentPage, pagination.pageSize, pagination.sortColumn || undefined, pagination.sortDirection || undefined);
setPagination(newPagination);
};
useEffect(() => {
if (!widget || !backend) return;
// Function to fetch cache from server
const fetchServerCache = async () => {
if (!backend || !widget) return null;
try {
const { data: cacheData } = await queue.add(() => backend.widget(widget.id).cache());
// Update component state
setCache(cacheData);
} catch (error) {
console.error("Error fetching widget cache from server:", error);
}
};
// Fetch data from server
fetchServerCache();
// Reset pagination when widget changes
return () => {
resetPagination();
};
}, [widget, backend]);
const layout_editable =
adminMode || dashboard?.settings?.can_edit_widget_layout;
const widget_editable = adminMode || dashboard?.settings?.can_edit_widgets;
const addable = adminMode || dashboard?.settings?.can_create_widgets;
const deletable = adminMode || dashboard?.settings?.can_delete_widgets;
const ImageDownloadEnabled = useMemo(() => {
if (adminMode) return true;
if (dashboard?.settings && dashboard.settings.disable_download_images)
return false;
return true;
}, [dashboard, widget, adminMode]);
const ReportDownloadEnabled = useMemo(() => {
if (adminMode) return true;
if (dashboard?.settings && dashboard.settings.disable_download_reports)
return false;
return true;
}, [dashboard, widget, adminMode]);
const error = useMemo(() => {
if (widget && widget.error && widget_editable) {
return (
setEditModalOpen(true, widget)} className="onvo-absolute onvo-right-4 onvo-bottom-4 onvo-rounded-full onvo-shadow-lg onvo-foreground-color onvo-z-10 onvo-border onvo-border-black/5 dark:onvo-border-white/10">
)
}
return <>>;
}, [widget]);
const moveDropdown = useMemo(() => {
const editable = adminMode || dashboard?.settings?.can_edit_tabs;
if ((dashboard?.tabs || []).length <= 1) {
return <>>;
}
if (!editable) return;
return (
Move widget
{dashboard?.tabs.map(a => (
moveTab(a.id)}>
{a.label}
))}
)
}, [tab, dashboard]);
const border_radius = useMemo(() => {
return dashboard?.settings?.widget_border_radius === undefined
? 16
: dashboard?.settings?.widget_border_radius;
}, [dashboard]);
if (widget.type === "divider") {
return (
{layout_editable && !hideOptions &&
}
{deletable && !hideOptions && (
{deletable && (
Delete widget
)}
)}
{footer}
);
}
if (widget.type === "text") {
let config = (widget.config as any);
let sub = config?.options?.plugins?.subtitle.text || "";
if (typeof sub !== "string") {
sub = sub.join("
");
}
return (
{layout_editable && !hideOptions &&
}
{widget.drilldown_widget && (
{
e.stopPropagation();
e.preventDefault();
if (widget.drilldown_widget) {
setDrilldownModalOpen(true, widget.drilldown_widget);
}
}}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onMouseUp={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="!onvo-rounded-full onvo-z-20 onvo-transition-all group-hover/chartcard:onvo-opacity-100 onvo-opacity-0 onvo-cursor-pointer !onvo-text-gray-500"
icon={FunnelIcon}
variant="shadow"
/>)}
{(widget_editable || deletable) && !hideOptions && (
{widget_editable && (
{
setTimeout(() => {
setTextModalOpen(true, widget);
}, 30);
}}
>
Edit widget
{moveDropdown}
)}
{(widget_editable) &&
ImageDownloadEnabled && }
{ImageDownloadEnabled && (
<>
Download images
exportWidget("svg")}>
Download as SVG
exportWidget("png")}>
Download as PNG
exportWidget("jpeg")}>
Download as JPEG
>
)}
{(widget_editable || ImageDownloadEnabled) && deletable && }
{deletable && (
Delete widget
)}
)}
{footer}
);
}
if (widget.type === "image") {
return (
{layout_editable && !hideOptions &&
}
{widget.drilldown_widget && (
{
e.stopPropagation();
e.preventDefault();
if (widget.drilldown_widget) {
setDrilldownModalOpen(true, widget.drilldown_widget);
}
}}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onMouseUp={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="!onvo-rounded-full onvo-z-20 onvo-transition-all group-hover/chartcard:onvo-opacity-100 onvo-opacity-0 onvo-cursor-pointer !onvo-text-gray-500"
icon={FunnelIcon}
variant="shadow"
/>)}
{(widget_editable || deletable) && !hideOptions && (
{widget_editable && (
{
setTimeout(() => {
setImageModalOpen(true, widget);
}, 30);
}}
>
Edit widget
{moveDropdown}
)}
{widget_editable && deletable && }
{deletable && (
Delete widget
)}
)}
{footer}
);
}
const paginationEnabled = widget.engine === "manual-v1" && widget.type === "table";
return (
{
const startX = e.clientX;
const startY = e.clientY;
const handleMouseUp = (upEvent: any) => {
if (!cardRef.current) return;
// Remove the event listener
// @ts-ignore
cardRef.current.removeEventListener('mouseup', handleMouseUp);
// Calculate distance between mousedown and mouseup
const deltaX = Math.abs(upEvent.clientX - startX);
const deltaY = Math.abs(upEvent.clientY - startY);
const threshold = 5; // 5px threshold for considering it a click vs. drag
// Only trigger if the mouse hasn't moved beyond the threshold
if (deltaX <= threshold && deltaY <= threshold) {
if (widget.settings?.link && widget.settings.link.trim() !== "") {
console.log("CLICKED: ", upEvent, widget.settings?.link);
window.open(widget.settings.link, "_blank");
}
}
};
// Add mouseup listener to document to catch mouseup events outside the element
// @ts-ignore
cardRef.current?.addEventListener('mouseup', handleMouseUp, { once: true });
}}
>
{error}
{layout_editable && !hideOptions && }
{widget.drilldown_widget && (
{
e.stopPropagation();
e.preventDefault();
if (widget.drilldown_widget) {
setDrilldownModalOpen(true, widget.drilldown_widget);
}
}}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onMouseUp={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="!onvo-rounded-full onvo-z-20 onvo-transition-all group-hover/chartcard:onvo-opacity-100 onvo-opacity-0 onvo-cursor-pointer !onvo-text-gray-500"
icon={FunnelIcon}
variant="shadow"
/>)}
{(addable ||
deletable ||
widget_editable ||
ImageDownloadEnabled ||
ReportDownloadEnabled) &&
!hideOptions && (
{
e.stopPropagation();
e.preventDefault();
}}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onMouseUp={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="!onvo-rounded-full onvo-z-20 onvo-transition-all group-hover/chartcard:onvo-opacity-100 onvo-opacity-0 onvo-cursor-pointer !onvo-text-gray-500"
icon={EllipsisVerticalIcon}
variant="shadow"
/>
{(addable || widget_editable) && (
<>
Edit widget
{widget_editable && (
<>
{
e.stopPropagation();
e.preventDefault();
setEditModalOpen(true, widget);
}}
>
Edit widget
{moveDropdown}>
)}
{addable && (
{
e.stopPropagation();
e.preventDefault();
duplicate();
}}>
Duplicate widget
)}
>
)}
{(addable || widget_editable) && ReportDownloadEnabled && (
)}
{ReportDownloadEnabled && (
<>
Download reports
{
e.stopPropagation();
e.preventDefault();
exportWidget("xlsx");
}}>
Download as excel
{
e.stopPropagation();
e.preventDefault();
exportWidget("csv");
}}>
Download as CSV
>
)}
{(addable || widget_editable || ReportDownloadEnabled) &&
ImageDownloadEnabled && }
{ImageDownloadEnabled && (
<>
Download images
{
e.stopPropagation();
e.preventDefault();
exportWidget("svg");
}}>
Download as SVG
{
e.stopPropagation();
e.preventDefault();
exportWidget("png");
}}>
Download as PNG
{
e.stopPropagation();
e.preventDefault();
exportWidget("jpeg");
}}>
Download as JPEG
>
)}
{(addable ||
widget_editable ||
ImageDownloadEnabled ||
ReportDownloadEnabled) &&
deletable && }
{deletable && (
{
e.stopPropagation();
e.preventDefault();
deleteWidget();
}}>
Delete widget
)}
)}
{(widget.code === "") && (
)}
{(!cache && widget.code !== "") && (
)}
{(cache && widget.code !== "") && (
)}
{footer}
{paginationEnabled && (
)}
);
};
export default ChartCard;