client/app/pages/dashboards/hooks/useDashboard.js (218 lines of code) (raw):

import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { isEmpty, includes, compact, map, has, pick, keys, extend, every, get } from "lodash"; import notification from "@/services/notification"; import location from "@/services/location"; import { Dashboard, collectDashboardFilters } from "@/services/dashboard"; import { currentUser } from "@/services/auth"; import recordEvent from "@/services/recordEvent"; import { QueryResultError } from "@/services/query"; import AddWidgetDialog from "@/components/dashboards/AddWidgetDialog"; import TextboxDialog from "@/components/dashboards/TextboxDialog"; import PermissionsEditorDialog from "@/components/PermissionsEditorDialog"; import { editableMappingsToParameterMappings, synchronizeWidgetTitles } from "@/components/ParameterMappingInput"; import ShareDashboardDialog from "../components/ShareDashboardDialog"; import useFullscreenHandler from "../../../lib/hooks/useFullscreenHandler"; import useRefreshRateHandler from "./useRefreshRateHandler"; import useEditModeHandler from "./useEditModeHandler"; export { DashboardStatusEnum } from "./useEditModeHandler"; function getAffectedWidgets(widgets, updatedParameters = []) { return !isEmpty(updatedParameters) ? widgets.filter(widget => Object.values(widget.getParameterMappings()) .filter(({ type }) => type === "dashboard-level") .some(({ mapTo }) => includes( updatedParameters.map(p => p.name), mapTo ) ) ) : widgets; } function useDashboard(dashboardData) { const [dashboard, setDashboard] = useState(dashboardData); const [filters, setFilters] = useState([]); const [refreshing, setRefreshing] = useState(false); const [gridDisabled, setGridDisabled] = useState(false); const globalParameters = useMemo(() => dashboard.getParametersDefs(), [dashboard]); const canEditDashboard = !dashboard.is_archived && dashboard.can_edit; const isDashboardOwnerOrAdmin = useMemo( () => !dashboard.is_archived && has(dashboard, "user.id") && (currentUser.id === dashboard.user.id || currentUser.hasPermission("admin")), [dashboard] ); const hasOnlySafeQueries = useMemo( () => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)), [dashboard] ); const managePermissions = useCallback(() => { const aclUrl = `api/dashboards/${dashboard.id}/acl`; PermissionsEditorDialog.showModal({ aclUrl, context: "dashboard", author: dashboard.user, }); }, [dashboard]); const updateDashboard = useCallback( (data, includeVersion = true) => { setDashboard(currentDashboard => extend({}, currentDashboard, data)); // for some reason the request uses the id as slug data = { ...data, slug: dashboard.id }; if (includeVersion) { data = { ...data, version: dashboard.version }; } return Dashboard.save(data) .then(updatedDashboard => setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data)))) ) .catch(error => { const status = get(error, "response.status"); if (status === 403) { notification.error("Dashboard update failed", "Permission Denied."); } else if (status === 409) { notification.error( "It seems like the dashboard has been modified by another user. ", "Please copy/backup your changes and reload this page.", { duration: null } ); } }); }, [dashboard] ); const togglePublished = useCallback(() => { recordEvent("toggle_published", "dashboard", dashboard.id); updateDashboard({ is_draft: !dashboard.is_draft }, false); }, [dashboard, updateDashboard]); const loadWidget = useCallback((widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL setDashboard(currentDashboard => extend({}, currentDashboard)); return widget .load(forceRefresh) .catch(error => { // QueryResultErrors are expected if (error instanceof QueryResultError) { return; } return Promise.reject(error); }) .finally(() => setDashboard(currentDashboard => extend({}, currentDashboard))); }, []); const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]); const removeWidget = useCallback(widgetId => { setDashboard(currentDashboard => extend({}, currentDashboard, { widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId), }) ); }, []); const dashboardRef = useRef(); dashboardRef.current = dashboard; const loadDashboard = useCallback( (forceRefresh = false, updatedParameters = []) => { const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters); const loadWidgetPromises = compact( affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error)) ); return Promise.all(loadWidgetPromises).then(() => { const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult())); const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search); setFilters(updatedFilters); }); }, [loadWidget] ); const refreshDashboard = useCallback( updatedParameters => { if (!refreshing) { setRefreshing(true); loadDashboard(true, updatedParameters).finally(() => setRefreshing(false)); } }, [refreshing, loadDashboard] ); const archiveDashboard = useCallback(() => { recordEvent("archive", "dashboard", dashboard.id); Dashboard.delete(dashboard).then(updatedDashboard => setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"]))) ); }, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps const showShareDashboardDialog = useCallback(() => { const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard)); ShareDashboardDialog.showModal({ dashboard, hasOnlySafeQueries, }) .onClose(handleDialogClose) .onDismiss(handleDialogClose); }, [dashboard, hasOnlySafeQueries]); const showAddTextboxDialog = useCallback(() => { TextboxDialog.showModal({ isNew: true, }).onClose(text => dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard))) ); }, [dashboard]); const showAddWidgetDialog = useCallback(() => { AddWidgetDialog.showModal({ dashboard, }).onClose(({ visualization, parameterMappings }) => dashboard .addWidget(visualization, { parameterMappings: editableMappingsToParameterMappings(parameterMappings), }) .then(widget => { const widgetsToSave = [ widget, ...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets), ]; return Promise.all(widgetsToSave.map(w => w.save())).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)) ); }) ); }, [dashboard]); const [refreshRate, setRefreshRate, disableRefreshRate] = useRefreshRateHandler(refreshDashboard); const [fullscreen, toggleFullscreen] = useFullscreenHandler(); const editModeHandler = useEditModeHandler(!gridDisabled && canEditDashboard, dashboard.widgets); useEffect(() => { setDashboard(dashboardData); loadDashboard(); }, [dashboardData]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { document.title = dashboard.name; }, [dashboard.name]); // reload dashboard when filter option changes useEffect(() => { loadDashboard(); }, [dashboard.dashboard_filters_enabled]); // eslint-disable-line react-hooks/exhaustive-deps return { dashboard, globalParameters, refreshing, filters, setFilters, loadDashboard, refreshDashboard, updateDashboard, togglePublished, archiveDashboard, loadWidget, refreshWidget, removeWidget, canEditDashboard, isDashboardOwnerOrAdmin, refreshRate, setRefreshRate, disableRefreshRate, ...editModeHandler, gridDisabled, setGridDisabled, fullscreen, toggleFullscreen, showShareDashboardDialog, showAddTextboxDialog, showAddWidgetDialog, managePermissions, }; } export default useDashboard;