diff --git a/frontend/src/global.scss b/frontend/src/global.scss index 9d4e3ea430fa5..d4c26b6d150b4 100644 --- a/frontend/src/global.scss +++ b/frontend/src/global.scss @@ -249,14 +249,13 @@ code.code { color: $text_default; font-family: inherit; background-color: $bg_light; -} + border: 1px solid $border; + @extend .text-default; + color: $text_default; -.Toastify__toast-body { - @extend .l3; - color: $success; - p { - @extend .text-default; - color: $text_default; + h1 { + font-size: 1.14rem; + font-weight: bold; } } @@ -264,6 +263,21 @@ code.code { background: $success; } +.Toastify__toast--success { + h1 { + color: $success; + } +} + +.Toastify__toast--info { + h1 { + color: $primary_alt; + } + &.accent-border { + border-color: $primary_alt; + } +} + .Toastify__toast--error { h1 { color: $danger; @@ -350,6 +364,15 @@ code.code { margin-left: 5px; } +.btn-lg-2x { + font-size: 1.5rem !important; + line-height: 1 !important; + svg { + width: 1.5rem !important; + height: 1.5rem !important; + } +} + // Badges styles .badge { border-radius: 50%; diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 5276005cc503a..96e7baf241149 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -6,6 +6,7 @@ import { userLogic } from 'scenes/userLogic' import { eventUsageLogicType } from './eventUsageLogicType' import { AnnotationType, FilterType, DashboardType, PersonType } from '~/types' import { ViewType } from 'scenes/insights/insightLogic' +import { Moment } from 'moment' const keyMappingKeys = Object.keys(keyMapping.event) @@ -14,7 +15,6 @@ export const eventUsageLogic = kea ({ annotations }), reportPersonDetailViewed: (person: PersonType) => ({ person }), reportInsightViewed: (filters: Partial, isFirstLoad: boolean) => ({ filters, isFirstLoad }), - reportDashboardViewed: (dashboard: DashboardType, hasShareToken: boolean) => ({ dashboard, hasShareToken }), reportBookmarkletDragged: true, reportIngestionBookmarkletCollapsible: (activePanels: string[]) => ({ activePanels }), reportProjectCreationSubmitted: (projectCount: number, nameLength: number) => ({ projectCount, nameLength }), @@ -47,6 +47,30 @@ export const eventUsageLogic = kea ({ action, totalProperties, oldPropertyType, newPropertyType }), + reportDashboardViewed: (dashboard: DashboardType, hasShareToken: boolean) => ({ dashboard, hasShareToken }), + reportDashboardEditModeToggled: ( + isOnEditMode: boolean, + source: 'long_press' | 'more_dropdown' | 'dashboard_header' | 'hotkey' | 'rename_input' | 'toast' | null + ) => ({ isOnEditMode, source }), + reportDashboardRefreshed: (lastRefreshed?: string | Moment | null) => ({ lastRefreshed }), + reportDashboardDateRangeChanged: (dateFrom?: string | Moment, dateTo?: string | Moment | null) => ({ + dateFrom, + dateTo, + }), + reportDashboardPinToggled: (pinned: boolean, source: 'more_dropdown' | 'main_nav' | 'dashboards_list') => ({ + pinned, + source, + }), + reportDashboardPresentationModeToggled: ( + isPresentationMode: boolean, + source: 'more_dropdown' | 'hotkey' | 'dashboard_header' | 'browser' | null + ) => ({ + isPresentationMode, + source, + }), + reportDashboardDropdownNavigation: true, + reportDashboardRenamed: (originalLength: number, newLength: number) => ({ originalLength, newLength }), + reportDashboardShareToggled: (isShared: boolean) => ({ isShared }), }, listeners: { reportAnnotationViewed: async ({ annotations }, breakpoint) => { @@ -238,5 +262,34 @@ export const eventUsageLogic = kea { + posthog.capture(`dashboard edit mode toggled`, { is_on_edit_mode: isOnEditMode, source }) + }, + reportDashboardRefreshed: async ({ lastRefreshed }) => { + posthog.capture(`dashboard refreshed`, { last_refreshed: lastRefreshed?.toString() }) + }, + reportDashboardDateRangeChanged: async ({ dateFrom, dateTo }) => { + posthog.capture(`dashboard date range changed`, { + date_from: dateFrom?.toString(), + date_to: dateTo?.toString(), + }) + }, + reportDashboardPinToggled: async ({ pinned, source }) => { + posthog.capture(`dashboard pin toggled`, { pinned: pinned, source }) + }, + reportDashboardPresentationModeToggled: async ({ isPresentationMode, source }) => { + posthog.capture(`dashboard presentation mode toggled`, { is_presentation_mode: isPresentationMode, source }) + }, + reportDashboardDropdownNavigation: async () => { + /* Triggered when a user navigates using the dropdown in the header. + */ + posthog.capture(`dashboard dropdown navigated`) + }, + reportDashboardRenamed: async ({ originalLength, newLength }) => { + posthog.capture(`dashboard renamed`, { original_length: originalLength, new_length: newLength }) + }, + reportDashboardShareToggled: async ({ isShared }) => { + posthog.capture(`dashboard share toggled`, { is_shared: isShared }) + }, }, }) diff --git a/frontend/src/models/dashboardsModel.js b/frontend/src/models/dashboardsModel.js index 207728d3ca584..57fbcb75591a8 100644 --- a/frontend/src/models/dashboardsModel.js +++ b/frontend/src/models/dashboardsModel.js @@ -2,6 +2,7 @@ import { kea } from 'kea' import { router } from 'kea-router' import api from 'lib/api' import { delay, idToKey } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React from 'react' import { toast } from 'react-toastify' @@ -12,8 +13,10 @@ export const dashboardsModel = kea({ // this is moved out of dashboardLogic, so that you can click "undo" on a item move when already // on another dashboard - both dashboards can listen to and share this event, even if one is not yet mounted updateDashboardItem: (item) => ({ item }), + pinDashboard: (id, source = null) => ({ id, source }), + unpinDashboard: (id, source = null) => ({ id, source }), }), - loaders: () => ({ + loaders: ({ values }) => ({ rawDashboards: [ {}, { @@ -41,13 +44,26 @@ export const dashboardsModel = kea({ } return result }, - renameDashboard: async ({ id, name }) => await api.update(`api/dashboard/${id}`, { name }), + renameDashboard: async ({ id, name }, breakpoint) => { + await breakpoint(700) + const response = await api.update(`api/dashboard/${id}`, { name }) + eventUsageLogic.actions.reportDashboardRenamed(values.rawDashboards[id].name.length, name.length) + return response + }, setIsSharedDashboard: async ({ id, isShared }) => await api.update(`api/dashboard/${id}`, { is_shared: isShared }), deleteDashboard: async ({ id }) => await api.update(`api/dashboard/${id}`, { deleted: true }), restoreDashboard: async ({ id }) => await api.update(`api/dashboard/${id}`, { deleted: false }), - pinDashboard: async (id) => await api.update(`api/dashboard/${id}`, { pinned: true }), - unpinDashboard: async (id) => await api.update(`api/dashboard/${id}`, { pinned: false }), + pinDashboard: async ({ id, source }) => { + const response = await api.update(`api/dashboard/${id}`, { pinned: true }) + eventUsageLogic.actions.reportDashboardPinToggled(true, source) + return response + }, + unpinDashboard: async ({ id, source }) => { + const response = await api.update(`api/dashboard/${id}`, { pinned: false }) + eventUsageLogic.actions.reportDashboardPinToggled(false, source) + return response + }, }, }), diff --git a/frontend/src/scenes/dashboard/Dashboard.scss b/frontend/src/scenes/dashboard/Dashboard.scss new file mode 100644 index 0000000000000..42d8557189288 --- /dev/null +++ b/frontend/src/scenes/dashboard/Dashboard.scss @@ -0,0 +1,111 @@ +@import '~/vars'; + +$dashboard-title-size: 32px; + +.dashboard { + margin-top: $default_spacing * 2; + + .dashboard-items-actions { + margin-bottom: $default_spacing; + display: flex; + + .left-item { + flex-grow: 1; + } + + .ant-btn { + padding-left: 4px !important; + } + } +} + +.dashboard-header { + display: flex; + justify-content: space-between; + + margin-top: -1rem; + margin-bottom: 2rem; + + width: 100%; + + &.full-screen { + margin-top: 1rem; + } + + .dashboard-select { + flex: 1; + @extend .text-ellipsis; + padding-right: $default_spacing * 2; + + .ant-select-single { + max-width: 100%; + line-height: $dashboard-title-size; + + .ant-select-selector { + padding-left: 0; + padding-right: 8px; + line-height: $dashboard-title-size; + height: $dashboard-title-size; + + .ant-select-selection-item { + font-size: $dashboard-title-size; + line-height: $dashboard-title-size; + width: 100%; + height: 35px; + display: block; + + .anticon-share-alt { + font-size: $dashboard-title-size * 0.65; + color: $success; + margin-left: 6px !important; + margin-right: 4px; + margin-top: $default_spacing / 2; + } + } + } + &.ant-select-open { + .ant-select-arrow { + color: rgba($text_default, 0.4); + } + } + .ant-select-arrow { + color: $text_default; + font-size: 0.5 * $dashboard-title-size; + } + } + } + .dashboard-meta { + white-space: nowrap; + display: flex; + align-items: center; + .ant-btn { + .anticon { + vertical-align: baseline; + } + margin-left: 10px; + &.button-box { + padding: 4px 8px; + } + @media (max-width: 750px) { + &.button-box-when-small { + padding: 4px 8px; + } + } + } + } + + .dashboard-header-created-by { + margin-top: $default_spacing / 2; + color: $text_muted; + } + + @media (max-width: 750px) { + .hide-when-small { + display: none; + } + + .dashboard-header-created-by { + display: none; /* header is already too crowded on mobile */ + } + } +} diff --git a/frontend/src/scenes/dashboard/Dashboard.tsx b/frontend/src/scenes/dashboard/Dashboard.tsx index e1e1c2a545bc6..1deffbe674995 100644 --- a/frontend/src/scenes/dashboard/Dashboard.tsx +++ b/frontend/src/scenes/dashboard/Dashboard.tsx @@ -1,13 +1,16 @@ import React from 'react' import { Link } from 'lib/components/Link' import { SceneLoading } from 'lib/utils' -import { BindLogic, useValues } from 'kea' -import { userLogic } from 'scenes/userLogic' +import { BindLogic, useActions, useValues } from 'kea' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { DashboardHeader } from 'scenes/dashboard/DashboardHeader' import { DashboardItems } from 'scenes/dashboard/DashboardItems' import { dashboardsModel } from '~/models/dashboardsModel' -import { HedgehogOverlay } from 'lib/components/HedgehogOverlay/HedgehogOverlay' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { CalendarOutlined, ReloadOutlined } from '@ant-design/icons' +import moment from 'moment' +import { Button } from 'antd' +import './Dashboard.scss' interface Props { id: string @@ -17,38 +20,64 @@ interface Props { export function Dashboard({ id, shareToken }: Props): JSX.Element { return ( - + ) } -function DashboardView({ id, shareToken }: Props): JSX.Element { - const { dashboard, itemsLoading, items } = useValues(dashboardLogic) - const { user } = useValues(userLogic) +function DashboardView(): JSX.Element { + const { dashboard, itemsLoading, items, isOnSharedMode, lastRefreshed, filters: dashboardFilters } = useValues( + dashboardLogic + ) const { dashboardsLoading } = useValues(dashboardsModel) + const { refreshAllDashboardItems, setDates } = useActions(dashboardLogic) + + if (dashboardsLoading || itemsLoading) { + return + } + + if (!dashboard) { + return ( + <> +

Dashboard not found.

+ + ) + } return ( -
- {!shareToken && } +
+ {!isOnSharedMode && } - {dashboardsLoading ? ( - - ) : !dashboard ? ( - <> -

A dashboard with the ID {id} was not found!

- - - ) : items && items.length > 0 ? ( - - ) : itemsLoading ? ( - - ) : user?.team?.ingested_event ? ( + {items && items.length ? ( +
+
+
+ Last updated {lastRefreshed ? moment(lastRefreshed).fromNow() : 'a while ago'} + +
+ ( + <> + + {key} + + )} + /> +
+ +
+ ) : (

There are no panels on this dashboard.{' '} Click here to add some!

- ) : ( -

)}

) diff --git a/frontend/src/scenes/dashboard/DashboardHeader.scss b/frontend/src/scenes/dashboard/DashboardHeader.scss deleted file mode 100644 index dec8335cbce25..0000000000000 --- a/frontend/src/scenes/dashboard/DashboardHeader.scss +++ /dev/null @@ -1,73 +0,0 @@ -@import '~/vars'; - -.dashboard-header { - display: flex; - justify-content: space-between; - - margin-top: -1rem; - margin-bottom: 2rem; - - --dashboard-title-size: 32px; - width: 100%; - - &.full-screen { - margin-top: 1rem; - } - - .dashboard-select { - flex: 1; - overflow: hidden; - .ant-select-single { - max-width: 100%; - line-height: var(--dashboard-title-size); - - .ant-select-selector { - padding-left: 0; - padding-right: 15px; - line-height: var(--dashboard-title-size); - height: var(--dashboard-title-size); - - .ant-select-selection-item { - font-size: var(--dashboard-title-size); - line-height: var(--dashboard-title-size); - width: 100%; - height: 35px; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - } - .dashboard-meta { - white-space: nowrap; - .ant-btn { - .anticon { - vertical-align: baseline; - } - margin-left: 10px; - &.button-box { - padding: 4px 8px; - } - @media (max-width: 750px) { - &.button-box-when-small { - padding: 4px 8px; - } - } - } - } - - .dashboard-header-created-by { - margin-top: $default_spacing / 2; - color: $text_muted; - } - - @media (max-width: 750px) { - .hide-when-small { - display: none; - } - - .dashboard-header-created-by { - display: none; /* header is already too crowded on mobile */ - } - } -} diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 31a805ff6a5fc..90d2a8bd24d77 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -1,7 +1,5 @@ -import './DashboardHeader.scss' - import { Loading, triggerResizeAfterADelay } from 'lib/utils' -import { Button, Dropdown, Menu, Select, Tooltip } from 'antd' +import { Button, Dropdown, Input, Menu, Select, Tooltip } from 'antd' import { router } from 'kea-router' import React, { useState } from 'react' import { useActions, useValues } from 'kea' @@ -15,167 +13,193 @@ import { DeleteOutlined, FullscreenOutlined, FullscreenExitOutlined, - LockOutlined, - UnlockOutlined, ShareAltOutlined, - ReloadOutlined, - CalendarOutlined, } from '@ant-design/icons' import { FullScreen } from 'lib/components/FullScreen' import moment from 'moment' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { DashboardType } from '~/types' -import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' export function DashboardHeader(): JSX.Element { - const { dashboard, draggingEnabled, filters: dashboardFilters } = useValues(dashboardLogic) - const { - addNewDashboard, - renameDashboard, - enableDragging, - disableDragging, - setDates, - refreshAllDashboardItems, - } = useActions(dashboardLogic) + const { dashboard, isOnEditMode } = useValues(dashboardLogic) + const { addNewDashboard, setIsOnEditMode, renameDashboard } = useActions(dashboardLogic) const { dashboards, dashboardsLoading } = useValues(dashboardsModel) const { pinDashboard, unpinDashboard, deleteDashboard } = useActions(dashboardsModel) const [fullScreen, setFullScreen] = useState(false) const [showShareModal, setShowShareModal] = useState(false) + const [newDashboardName, setNewDashboardName] = useState(dashboard.name) + const { reportDashboardPresentationModeToggled } = useActions(eventUsageLogic) + + const togglePresentationMode = ( + source: 'more_dropdown' | 'hotkey' | 'dashboard_header' | 'browser' | null, + newMode?: boolean + ): void => { + const _newMode = newMode !== undefined ? newMode : !fullScreen + setFullScreen(_newMode) + triggerResizeAfterADelay() + reportDashboardPresentationModeToggled(_newMode, source) + } + + const actionsDefault = ( + <> + + {dashboard.created_by && ( + <> + + Created by {dashboard.created_by.first_name || dashboard.created_by.email || '-'} on{' '} + {moment(dashboard.created_at).format( + moment(dashboard.created_at).year() === moment().year() + ? 'MMMM Do' + : 'MMMM Do YYYY' + )} + + + + )} + } onClick={() => setIsOnEditMode(true, 'more_dropdown')}> + Edit mode + + } + onClick={() => togglePresentationMode('more_dropdown')} + > + Presentation mode + + {dashboard.pinned ? ( + } + onClick={() => unpinDashboard(dashboard.id, 'more_dropdown')} + > + Unpin dashboard + + ) : ( + } + onClick={() => pinDashboard(dashboard.id, 'more_dropdown')} + > + Pin dashboard + + )} + + + } + onClick={() => deleteDashboard({ id: dashboard.id, redirect: true })} + danger + > + Delete dashboard + + + } + placement="bottomRight" + > + + + ) + + const actionsPresentationMode = ( + + ) + + const actionsEditMode = ( + + ) return (
- {fullScreen ? setFullScreen(false)} /> : null} + {fullScreen ? togglePresentationMode('browser', false)} /> : null} {showShareModal && setShowShareModal(false)} />} {dashboardsLoading ? ( ) : ( <> -
- - {dashboard.created_by ? ( -
- Created by {dashboard.created_by.first_name || dashboard.created_by.email || '-'} on{' '} - {moment(dashboard.created_at).format( - moment(dashboard.created_at).year() === moment().year() ? 'MMMM Do' : 'MMMM Do YYYY' - )} -
- ) : null} -
- {dashboard ? ( -
- - ( - <> - - {key} - - )} - /> - - - {!fullScreen ? ( - - - - ) : null} - - - - - - - - - - - - - - - - - {!fullScreen ? ( - - } onClick={renameDashboard}> - Rename "{dashboard.name}" - - } - onClick={() => deleteDashboard({ id: dashboard.id, redirect: true })} - className="text-danger" - > - Delete - - + {isOnEditMode ? ( + { + setNewDashboardName(e.target.value) // To update the input immediately + renameDashboard(e.target.value) // This is breakpointed (i.e. debounced) to avoid multiple API calls + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + setIsOnEditMode(false, 'rename_input') + } + }} + /> + ) : ( +
+
- ) : null} + )} + +
+ {isOnEditMode ? ( + <>{actionsEditMode} + ) : !fullScreen ? ( + <>{actionsDefault} + ) : ( + <>{actionsPresentationMode} + )} +
)}
diff --git a/frontend/src/scenes/dashboard/DashboardItem.tsx b/frontend/src/scenes/dashboard/DashboardItem.tsx index 9352b96c120a7..0ee033c2c618c 100644 --- a/frontend/src/scenes/dashboard/DashboardItem.tsx +++ b/frontend/src/scenes/dashboard/DashboardItem.tsx @@ -22,14 +22,12 @@ import { BlockOutlined, CopyOutlined, DeliveredProcedureOutlined, - ReloadOutlined, BarChartOutlined, SaveOutlined, } from '@ant-design/icons' import { dashboardColorNames, dashboardColors } from 'lib/colors' import { useLongPress } from 'lib/hooks/useLongPress' import { usePrevious } from 'lib/hooks/usePrevious' -import moment from 'moment' import { logicFromInsight, ViewType } from 'scenes/insights/insightLogic' import { dashboardsModel } from '~/models' import { RetentionContainer } from 'scenes/retention/RetentionContainer' @@ -45,10 +43,10 @@ interface Props { loadDashboardItems?: () => void isDraggingRef?: RefObject inSharedMode?: boolean - enableWobblyDragging?: () => void + isOnEditMode: boolean + setEditMode?: () => void index: number layout?: any - onRefresh?: () => void footer?: JSX.Element onClick?: () => void preventLoading?: boolean @@ -164,10 +162,10 @@ export function DashboardItem({ loadDashboardItems, isDraggingRef, inSharedMode, - enableWobblyDragging, + isOnEditMode, + setEditMode, index, layout, - onRefresh, footer, onClick, preventLoading, @@ -197,7 +195,7 @@ export function DashboardItem({ const { renameDashboardItem } = useActions(dashboardItemsModel) const otherDashboards: DashboardType[] = dashboards.filter((d: DashboardType) => d.id !== dashboardId) - const longPressProps = useLongPress(enableWobblyDragging, { + const longPressProps = useLongPress(setEditMode, { ms: 500, touch: true, click: false, @@ -212,7 +210,6 @@ export function DashboardItem({ preventLoading, } - const { loadResults } = useActions(logicFromInsight(item.filters.insight, logicProps)) const { results, resultsLoading } = useValues(logicFromInsight(item.filters.insight, logicProps)) const previousLoading = usePrevious(resultsLoading) @@ -220,8 +217,6 @@ export function DashboardItem({ useEffect(() => { if (previousLoading && !resultsLoading && !initialLoaded) { setInitialLoaded(true) - } else if (previousLoading && !resultsLoading && initialLoaded) { - onRefresh && onRefresh() } }, [resultsLoading]) @@ -240,7 +235,7 @@ export function DashboardItem({
)}
-
+
{inSharedMode ? ( item.name @@ -294,19 +289,6 @@ export function DashboardItem({ /> ))} - - Refreshed:{' '} - {item.last_refresh ? moment(item.last_refresh).fromNow() : 'just now'} - - } - > - loadResults(true)} - /> - {} export function DashboardItems({ inSharedMode }: { inSharedMode: boolean }): JSX.Element { - const { dashboard, items, layouts, layoutForItem, breakpoints, cols, draggingEnabled } = useValues(dashboardLogic) - const { - loadDashboardItems, - refreshDashboardItem, - updateLayouts, - updateContainerWidth, - updateItemColor, - enableWobblyDragging, - } = useActions(dashboardLogic) + const { dashboard, items, layouts, layoutForItem, breakpoints, cols, isOnEditMode } = useValues(dashboardLogic) + const { loadDashboardItems, updateLayouts, updateContainerWidth, updateItemColor, setIsOnEditMode } = useActions( + dashboardLogic + ) const { duplicateDashboardItem } = useActions(dashboardItemsModel) // make sure the dashboard takes up the right size @@ -32,14 +26,13 @@ export function DashboardItems({ inSharedMode }: { inSharedMode: boolean }): JSX // can not click links when dragging and 250ms after const isDragging = useRef(false) const dragEndTimeout = useRef(null) + const className = 'layout' + (isOnEditMode ? ' dragging-items wobbly' : '') return ( setIsOnEditMode(true, 'long_press')} index={index} - onRefresh={() => refreshDashboardItem(item.id)} />
))} diff --git a/frontend/src/scenes/dashboard/Dashboards.tsx b/frontend/src/scenes/dashboard/Dashboards.tsx index 65623b4cb15cf..e4b7b3bb15551 100644 --- a/frontend/src/scenes/dashboard/Dashboards.tsx +++ b/frontend/src/scenes/dashboard/Dashboards.tsx @@ -26,7 +26,9 @@ export function Dashboards(): JSX.Element { render: function RenderPin({ id, pinned }: DashboardType) { return ( (pinned ? unpinDashboard(id) : pinDashboard(id))} + onClick={() => + pinned ? unpinDashboard(id, 'dashboards_list') : pinDashboard(id, 'dashboards_list') + } style={{ color: 'rgba(0, 0, 0, 0.85)', cursor: 'pointer' }} > {pinned ? : } diff --git a/frontend/src/scenes/dashboard/dashboardLogic.js b/frontend/src/scenes/dashboard/dashboardLogic.js index b8568ff51754f..e9b6f11bb15fb 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.js +++ b/frontend/src/scenes/dashboard/dashboardLogic.js @@ -4,13 +4,13 @@ import { dashboardsModel } from '~/models/dashboardsModel' import { prompt } from 'lib/logic/prompt' import { router } from 'kea-router' import { toast } from 'react-toastify' -import { Link } from 'lib/components/Link' import React from 'react' -import { isAndroidOrIOS, clearDOMTextSelection, toParams } from 'lib/utils' +import { clearDOMTextSelection, toParams } from 'lib/utils' import { dashboardItemsModel } from '~/models/dashboardItemsModel' import { PATHS_VIZ, ACTIONS_LINE_GRAPH_LINEAR } from 'lib/constants' import { ViewType } from 'scenes/insights/insightLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { Button } from 'antd' export const dashboardLogic = kea({ connect: [dashboardsModel, dashboardItemsModel, eventUsageLogic], @@ -19,16 +19,14 @@ export const dashboardLogic = kea({ actions: () => ({ addNewDashboard: true, - renameDashboard: true, - setIsSharedDashboard: (id, isShared) => ({ id, isShared }), + renameDashboard: (name) => ({ name }), + setIsSharedDashboard: (id, isShared) => ({ id, isShared }), // whether the dashboard is shared or not + setIsOnSharedMode: (isOnSharedMode) => ({ isOnSharedMode }), // whether the dashboard is open in shared mode (i.e. with a shareToken) updateLayouts: (layouts) => ({ layouts }), updateContainerWidth: (containerWidth, columns) => ({ containerWidth, columns }), saveLayouts: true, updateItemColor: (id, color) => ({ id, color }), - enableDragging: true, - enableWobblyDragging: true, - disableDragging: true, - refreshDashboardItem: (id) => ({ id }), + setIsOnEditMode: (isOnEditMode, source = null) => ({ isOnEditMode, source }), refreshAllDashboardItems: true, updateAndRefreshDashboard: true, setDates: (dateFrom, dateTo, reloadDashboard = true) => ({ dateFrom, dateTo, reloadDashboard }), @@ -48,7 +46,6 @@ export const dashboardLogic = kea({ return dashboard } catch (error) { if (error.status === 404) { - // silently escape return [] } throw error @@ -101,12 +98,10 @@ export const dashboardLogic = kea({ return { ...state, items: item.dashboard === parseInt(props.id) ? [...state.items, item] : state.items } }, }, - draggingEnabled: [ - () => (isAndroidOrIOS() ? 'off' : 'on'), + columns: [ + null, { - enableDragging: () => 'on', - enableWobblyDragging: () => 'wobbly', - disableDragging: () => 'off', + updateContainerWidth: (_, { columns }) => columns, }, ], containerWidth: [ @@ -115,21 +110,43 @@ export const dashboardLogic = kea({ updateContainerWidth: (_, { containerWidth }) => containerWidth, }, ], - columns: [ - null, + isOnEditMode: [ + false, { - updateContainerWidth: (_, { columns }) => columns, + setIsOnEditMode: (_, { isOnEditMode }) => isOnEditMode, + }, + ], + isOnSharedMode: [ + false, + { + setIsOnSharedMode: (_, { isOnSharedMode }) => isOnSharedMode, }, ], }), selectors: ({ props, selectors }) => ({ items: [() => [selectors.allItems], (allItems) => allItems?.items?.filter((i) => !i.deleted)], itemsLoading: [() => [selectors.allItemsLoading], (allItemsLoading) => allItemsLoading], + lastRefreshed: [ + () => [selectors.items], + (items) => { + if (!items || !items.length) { + return null + } + let lastRefreshed = items[0].last_refresh + + for (const item of items) { + if (item.last_refresh < lastRefreshed) { + lastRefreshed = item.last_refresh + } + } + + return lastRefreshed + }, + ], dashboard: [ - () => [selectors.allItems, dashboardsModel.selectors.dashboards], - (allItems, dashboards) => { - let dashboard = dashboards.find((d) => d.id === props.id) || false - return dashboard ? dashboard : allItems + () => [dashboardsModel.selectors.dashboards], + (dashboards) => { + return dashboards.find((d) => d.id === props.id) }, ], breakpoints: [() => [], () => ({ lg: 1600, sm: 940, xs: 480, xxs: 0 })], @@ -232,8 +249,11 @@ export const dashboardLogic = kea({ }, ], }), - events: ({ actions, cache }) => ({ - afterMount: [actions.loadDashboardItems], + events: ({ actions, cache, props }) => ({ + afterMount: () => { + actions.loadDashboardItems() + actions.setIsOnSharedMode(!!props.shareToken) + }, beforeUnmount: () => { if (cache.draggingToastId) { toast.dismiss(cache.draggingToastId) @@ -241,7 +261,6 @@ export const dashboardLogic = kea({ } }, }), - listeners: ({ actions, values, key, cache }) => ({ addNewDashboard: async () => { prompt({ key: `new-dashboard-${key}` }).actions.prompt({ @@ -252,29 +271,19 @@ export const dashboardLogic = kea({ success: (name) => dashboardsModel.actions.addDashboard({ name }), }) }, - [dashboardsModel.actions.addDashboardSuccess]: ({ dashboard }) => { router.actions.push(`/dashboard/${dashboard.id}`) }, - setIsSharedDashboard: ({ id, isShared }) => { dashboardsModel.actions.setIsSharedDashboard({ id, isShared }) + eventUsageLogic.actions.reportDashboardShareToggled(isShared) }, - - renameDashboard: async () => { - prompt({ key: `rename-dashboard-${key}` }).actions.prompt({ - title: 'Rename dashboard', - placeholder: 'Please enter the new name', - value: values.dashboard.name, - error: 'You must enter name', - success: (name) => dashboardsModel.actions.renameDashboard({ id: values.dashboard.id, name }), - }) + renameDashboard: ({ name }) => { + dashboardsModel.actions.renameDashboard({ id: values.dashboard.id, name }) }, - updateLayouts: () => { actions.saveLayouts() }, - saveLayouts: async (_, breakpoint) => { await breakpoint(300) await api.update(`api/dashboard_item/layouts`, { @@ -288,56 +297,13 @@ export const dashboardLogic = kea({ }), }) }, - updateItemColor: ({ id, color }) => { api.update(`api/insight/${id}`, { color }) }, - - enableWobblyDragging: () => { - clearDOMTextSelection() - window.setTimeout(clearDOMTextSelection, 200) - window.setTimeout(clearDOMTextSelection, 1000) - - if (!cache.draggingToastId) { - cache.draggingToastId = toast( - <> -

Rearranging panels!

-

- actions.disableDragging()}>Click here to stop. -

- , - { - autoClose: false, - onClick: () => actions.disableDragging(), - closeButton: false, - className: 'drag-items-toast', - } - ) - } - }, - enableDragging: () => { - if (cache.draggingToastId) { - toast.dismiss(cache.draggingToastId) - cache.draggingToastId = null - } - }, - disableDragging: () => { - if (cache.draggingToastId) { - toast.dismiss(cache.draggingToastId) - cache.draggingToastId = null - } - }, - refreshDashboardItem: async ({ id }, breakpoint) => { - const dashboardItem = await api.get(`api/insight/${id}`) - await breakpoint() - dashboardsModel.actions.updateDashboardItem(dashboardItem) - if (dashboardItem.refreshing) { - setTimeout(() => actions.refreshDashboardItem(id), 1000) - } - }, refreshAllDashboardItems: async (_, breakpoint) => { await breakpoint(200) dashboardItemsModel.actions.refreshAllDashboardItems({}) + eventUsageLogic.actions.reportDashboardRefreshed(values.lastRefreshed) }, updateAndRefreshDashboard: async (_, breakpoint) => { await breakpoint(200) @@ -348,6 +314,39 @@ export const dashboardLogic = kea({ if (reloadDashboard) { actions.updateAndRefreshDashboard() } + eventUsageLogic.actions.reportDashboardDateRangeChanged(values.filters.date_from, values.filters.date_to) + }, + setIsOnEditMode: ({ isOnEditMode, source }) => { + if (isOnEditMode) { + clearDOMTextSelection() + window.setTimeout(clearDOMTextSelection, 200) + window.setTimeout(clearDOMTextSelection, 1000) + + if (!cache.draggingToastId) { + cache.draggingToastId = toast( + <> +

Dashboard edit mode

+

Tap below when finished.

+
+ +
+ , + { + type: 'info', + autoClose: false, + onClick: () => actions.setIsOnEditMode(false, 'toast'), + closeButton: false, + className: 'drag-items-toast accent-border', + } + ) + } + } else { + if (cache.draggingToastId) { + toast.dismiss(cache.draggingToastId) + cache.draggingToastId = null + } + } + eventUsageLogic.actions.reportDashboardEditModeToggled(isOnEditMode, source) }, }), }) diff --git a/frontend/src/scenes/insights/InsightHistoryPanel/InsightHistoryPanel.tsx b/frontend/src/scenes/insights/InsightHistoryPanel/InsightHistoryPanel.tsx index 0c6963f071007..e2b366e0af829 100644 --- a/frontend/src/scenes/insights/InsightHistoryPanel/InsightHistoryPanel.tsx +++ b/frontend/src/scenes/insights/InsightHistoryPanel/InsightHistoryPanel.tsx @@ -77,6 +77,7 @@ function InsightPane({ preventLoading={true} footer={
{footer(insight)}
} index={index} + isOnEditMode={false} /> ))} diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index c7404544b6859..747656334ece5 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -80,17 +80,14 @@ export const scenes: Record any> = { interface SceneConfig { onlyUnauthenticated?: boolean // Route should only be accessed when logged out (N.B. should be added to posthog/urls.py too) - allowUnauthenticated?: boolean // Route **can** be accessed when logged out (i.e. can be accessed when logged in too) + allowUnauthenticated?: boolean // Route **can** be accessed when logged out (i.e. can be accessed when logged in too; should be added to posthog/urls.py too) dark?: boolean // Background is $bg_mid plain?: boolean // Only keeps the main content and the top navigation bar hideTopNav?: boolean // Hides the top navigation bar (regardless of whether `plain` is `true` or not) - hideDemoWarnings?: boolean // Hides demo project (DemoWarning.tsx) + hideDemoWarnings?: boolean // Hides demo project warnings (DemoWarning.tsx) } export const sceneConfigurations: Partial> = { - [Scene.Dashboard]: { - dark: true, - }, [Scene.Insights]: { dark: true, },