From 9d860ed86bcd75a60e76101a5312261f0a206a05 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Mon, 23 Dec 2024 09:52:21 -0800 Subject: [PATCH 01/11] feat: add insight and remove insight from dashoard --- .../dashboard/AddInsightToDashboardModal.tsx | 38 ++ .../src/scenes/dashboard/DashboardHeader.tsx | 9 +- .../dashboard/EmptyDashboardComponent.tsx | 12 +- .../addInsightToDasboardModalLogic.ts | 20 + frontend/src/scenes/insights/insightLogic.tsx | 1 - .../AddSavedInsightsToDashboard.tsx | 562 ++++++++++++++++++ .../saved-insights/savedInsightsLogic.ts | 33 + 7 files changed, 666 insertions(+), 9 deletions(-) create mode 100644 frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx create mode 100644 frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts create mode 100644 frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx diff --git a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx new file mode 100644 index 0000000000000..62c32ddad4db9 --- /dev/null +++ b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx @@ -0,0 +1,38 @@ +import { useActions, useValues } from 'kea' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonModal } from 'lib/lemon-ui/LemonModal' +import { addInsightToDashboardLogic } from 'scenes/dashboard/addInsightToDasboardModalLogic' +import { AddSavedInsightsToDashboard } from 'scenes/saved-insights/AddSavedInsightsToDashboard' +import { urls } from 'scenes/urls' + +import { dashboardLogic } from './dashboardLogic' + +export function AddInsightToDashboardModal(): JSX.Element { + const { hideAddInsightToDashboardModal } = useActions(addInsightToDashboardLogic) + const { addInsightToDashboardModalVisible } = useValues(addInsightToDashboardLogic) + const { dashboard } = useValues(dashboardLogic) + return ( + + + Cancel + + + New insight + + + } + > + {/*

Add insight to dashboard {dashboard?.name}

*/} + +
+ ) +} diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 403827fde6668..2539557188c67 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -28,6 +28,8 @@ import { notebooksModel } from '~/models/notebooksModel' import { tagsModel } from '~/models/tagsModel' import { DashboardMode, DashboardType, ExporterFormat, QueryBasedInsightModel } from '~/types' +import { addInsightToDashboardLogic } from './addInsightToDasboardModalLogic' +import { AddInsightToDashboardModal } from './AddInsightToDashboardModal' import { DASHBOARD_RESTRICTION_OPTIONS } from './DashboardCollaborators' import { dashboardCollaboratorsLogic } from './dashboardCollaboratorsLogic' import { dashboardLogic } from './dashboardLogic' @@ -53,7 +55,7 @@ export function DashboardHeader(): JSX.Element | null { const { asDashboardTemplate } = useValues(dashboardLogic) const { updateDashboard, pinDashboard, unpinDashboard } = useActions(dashboardsModel) const { createNotebookFromDashboard } = useActions(notebooksModel) - + const { showAddInsightToDashboardModal } = useActions(addInsightToDashboardLogic) const { setDashboardTemplate, openDashboardTemplateEditor } = useActions(dashboardTemplateEditorLogic) const { user } = useValues(userLogic) @@ -114,6 +116,7 @@ export function DashboardHeader(): JSX.Element | null { )} {canEditDashboard && } {canEditDashboard && } + {canEditDashboard && } )} @@ -290,7 +293,9 @@ export function DashboardHeader(): JSX.Element | null { )} {dashboard ? ( { + showAddInsightToDashboardModal() + }} type="primary" data-attr="dashboard-add-graph-header" disabledReason={canEditDashboard ? null : DASHBOARD_CANNOT_EDIT_MESSAGE} diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx index 8265bac558368..6558513ed8ef7 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx @@ -1,14 +1,13 @@ import './EmptyDashboardComponent.scss' import { IconPlus } from '@posthog/icons' -import { useValues } from 'kea' +import { useActions } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import React from 'react' -import { urls } from 'scenes/urls' +import { addInsightToDashboardLogic } from './addInsightToDasboardModalLogic' import { DASHBOARD_CANNOT_EDIT_MESSAGE } from './DashboardHeader' -import { dashboardLogic } from './dashboardLogic' function SkeletonCard({ children, active }: { children: React.ReactNode; active: boolean }): JSX.Element { return ( @@ -77,8 +76,7 @@ function SkeletonCardTwo({ active }: { active: boolean }): JSX.Element { } export function EmptyDashboardComponent({ loading, canEdit }: { loading: boolean; canEdit: boolean }): JSX.Element { - const { dashboard } = useValues(dashboardLogic) - + const { showAddInsightToDashboardModal } = useActions(addInsightToDashboardLogic) return (
{!loading && ( @@ -88,7 +86,9 @@ export function EmptyDashboardComponent({ loading, canEdit }: { loading: boolean
{ + showAddInsightToDashboardModal() + }} type="primary" icon={} center diff --git a/frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts b/frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts new file mode 100644 index 0000000000000..2bf2b03a3e01f --- /dev/null +++ b/frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts @@ -0,0 +1,20 @@ +import { actions, kea, path, reducers } from 'kea' + +import type { addInsightToDashboardLogicType } from './addInsightToDasboardModalLogicType' + +export const addInsightToDashboardLogic = kea([ + path(['scenes', 'dashboard', 'addInsightToDashboardLogic']), + actions({ + showAddInsightToDashboardModal: true, + hideAddInsightToDashboardModal: true, + }), + reducers({ + addInsightToDashboardModalVisible: [ + false, + { + showAddInsightToDashboardModal: () => true, + hideAddInsightToDashboardModal: () => false, + }, + ], + }), +]) diff --git a/frontend/src/scenes/insights/insightLogic.tsx b/frontend/src/scenes/insights/insightLogic.tsx index 1ca1548f30047..d36063066ae29 100644 --- a/frontend/src/scenes/insights/insightLogic.tsx +++ b/frontend/src/scenes/insights/insightLogic.tsx @@ -121,7 +121,6 @@ export const insightLogic: LogicWrapper = kea JSX.Element | null + inMenu: boolean +} + +export const INSIGHT_TYPES_METADATA: Record = { + [InsightType.TRENDS]: { + name: 'Trends', + description: 'Visualize and break down how actions or events vary over time.', + icon: IconTrends, + inMenu: true, + }, + [InsightType.FUNNELS]: { + name: 'Funnel', + description: 'Discover how many users complete or drop out of a sequence of actions.', + icon: IconFunnels, + inMenu: true, + }, + [InsightType.RETENTION]: { + name: 'Retention', + description: 'See how many users return on subsequent days after an initial action.', + icon: IconRetention, + inMenu: true, + }, + [InsightType.PATHS]: { + name: 'Paths', + description: 'Trace the journeys users take within your product and where they drop off.', + icon: IconUserPaths, + inMenu: true, + }, + [InsightType.STICKINESS]: { + name: 'Stickiness', + description: 'See what keeps users coming back by viewing the interval between repeated actions.', + icon: IconStickiness, + inMenu: true, + }, + [InsightType.LIFECYCLE]: { + name: 'Lifecycle', + description: 'Understand growth by breaking down new, resurrected, returning and dormant users.', + icon: IconLifecycle, + inMenu: true, + }, + [InsightType.SQL]: { + name: 'SQL', + description: 'Use HogQL to query your data.', + icon: IconHogQL, + inMenu: true, + }, + [InsightType.JSON]: { + name: 'Custom', + description: 'Save components powered by our JSON query language.', + icon: IconBrackets, + inMenu: true, + }, + [InsightType.HOG]: { + name: 'Hog', + description: 'Use Hog to query your data.', + icon: IconHogQL, + inMenu: true, + }, +} + +export const QUERY_TYPES_METADATA: Record = { + [NodeKind.TrendsQuery]: { + name: 'Trends', + description: 'Visualize and break down how actions or events vary over time', + icon: IconTrends, + inMenu: true, + }, + [NodeKind.FunnelsQuery]: { + name: 'Funnel', + description: 'Discover how many users complete or drop out of a sequence of actions', + icon: IconFunnels, + inMenu: true, + }, + [NodeKind.RetentionQuery]: { + name: 'Retention', + description: 'See how many users return on subsequent days after an initial action', + icon: IconRetention, + inMenu: true, + }, + [NodeKind.PathsQuery]: { + name: 'Paths', + description: 'Trace the journeys users take within your product and where they drop off', + icon: IconUserPaths, + inMenu: true, + }, + [NodeKind.StickinessQuery]: { + name: 'Stickiness', + description: 'See what keeps users coming back by viewing the interval between repeated actions', + icon: IconStickiness, + inMenu: true, + }, + [NodeKind.LifecycleQuery]: { + name: 'Lifecycle', + description: 'Understand growth by breaking down new, resurrected, returning and dormant users', + icon: IconLifecycle, + inMenu: true, + }, + [NodeKind.FunnelCorrelationQuery]: { + name: 'Funnel Correlation', + description: 'See which events or properties correlate to a funnel result', + icon: IconCorrelationAnalysis, + inMenu: false, + }, + [NodeKind.EventsNode]: { + name: 'Events', + description: 'List and explore events', + icon: IconCursor, + inMenu: true, + }, + [NodeKind.ActionsNode]: { + name: 'Actions', + description: 'List and explore actions', + icon: IconAction, + inMenu: true, + }, + [NodeKind.DataWarehouseNode]: { + name: 'Data Warehouse', + description: 'List and explore data warehouse tables', + icon: IconTableChart, + inMenu: true, + }, + [NodeKind.EventsQuery]: { + name: 'Events Query', + description: 'List and explore events', + icon: IconCursor, + inMenu: true, + }, + [NodeKind.PersonsNode]: { + name: 'Persons', + description: 'List and explore your persons', + icon: IconPerson, + inMenu: true, + }, + [NodeKind.ActorsQuery]: { + name: 'Persons', + description: 'List of persons matching specified conditions', + icon: IconPerson, + inMenu: false, + }, + [NodeKind.InsightActorsQuery]: { + name: 'Persons', + description: 'List of persons matching specified conditions, derived from an insight', + icon: IconPerson, + inMenu: false, + }, + [NodeKind.InsightActorsQueryOptions]: { + name: 'Persons', + description: 'Options for InsightActorsQueryt', + icon: IconPerson, + inMenu: false, + }, + [NodeKind.FunnelsActorsQuery]: { + name: 'Persons', + description: 'List of persons matching specified conditions, derived from an insight', + icon: IconPerson, + inMenu: false, + }, + [NodeKind.FunnelCorrelationActorsQuery]: { + name: 'Persons', + description: 'List of persons matching specified conditions, derived from an insight', + icon: IconPerson, + inMenu: false, + }, + [NodeKind.DataTableNode]: { + name: 'Data table', + description: 'Slice and dice your data in a table', + icon: IconTableChart, + inMenu: true, + }, + [NodeKind.DataVisualizationNode]: { + name: 'Data visualization', + description: 'Slice and dice your data in a table or chart', + icon: IconTableChart, + inMenu: false, + }, + [NodeKind.SavedInsightNode]: { + name: 'Insight visualization by short id', + description: 'View your insights', + icon: IconGraph, + inMenu: true, + }, + [NodeKind.InsightVizNode]: { + name: 'Insight visualization', + description: 'View your insights', + icon: IconGraph, + inMenu: true, + }, + [NodeKind.SessionsTimelineQuery]: { + name: 'Sessions', + description: 'Sessions timeline query', + icon: IconTrends, + inMenu: true, + }, + [NodeKind.HogQLQuery]: { + name: 'HogQL', + description: 'Direct HogQL query', + icon: IconBrackets, + inMenu: true, + }, + [NodeKind.HogQLMetadata]: { + name: 'HogQL Metadata', + description: 'Metadata for a HogQL query', + icon: IconHogQL, + inMenu: true, + }, + [NodeKind.HogQLAutocomplete]: { + name: 'HogQL Autocomplete', + description: 'Autocomplete for the HogQL query editor', + icon: IconHogQL, + inMenu: false, + }, + [NodeKind.DatabaseSchemaQuery]: { + name: 'Database Schema', + description: 'Introspect the PostHog database schema', + icon: IconHogQL, + inMenu: true, + }, + [NodeKind.WebOverviewQuery]: { + name: 'Overview Stats', + description: 'View overview stats for a website', + icon: IconPieChart, + inMenu: true, + }, + [NodeKind.WebStatsTableQuery]: { + name: 'Web Table', + description: 'A table of results from web analytics, with a breakdown', + icon: IconPieChart, + inMenu: true, + }, + [NodeKind.WebGoalsQuery]: { + name: 'Goals', + description: 'View goal conversions', + icon: IconPieChart, + inMenu: true, + }, + [NodeKind.WebExternalClicksTableQuery]: { + name: 'External click urls', + description: 'View clicks on external links', + icon: IconPieChart, + inMenu: true, + }, + [NodeKind.HogQuery]: { + name: 'Hog', + description: 'Hog query', + icon: IconHogQL, + inMenu: true, + }, + [NodeKind.SessionAttributionExplorerQuery]: { + name: 'Session Attribution', + description: 'Session Attribution Explorer', + icon: IconPieChart, + inMenu: true, + }, + [NodeKind.ErrorTrackingQuery]: { + name: 'Error Tracking', + description: 'List and explore exception groups', + icon: IconWarning, + inMenu: false, + }, + [NodeKind.RecordingsQuery]: { + name: 'Session Recordings', + description: 'View available recordings', + icon: IconVideoCamera, + inMenu: false, + }, + [NodeKind.ExperimentTrendsQuery]: { + name: 'Experiment Trends Result', + description: 'View experiment trend result', + icon: IconFlask, + inMenu: false, + }, + [NodeKind.ExperimentFunnelsQuery]: { + name: 'Experiment Funnels Result', + description: 'View experiment funnel result', + icon: IconFlask, + inMenu: false, + }, + [NodeKind.TeamTaxonomyQuery]: { + name: 'Team Taxonomy', + icon: IconHogQL, + inMenu: false, + }, + [NodeKind.EventTaxonomyQuery]: { + name: 'Event Taxonomy', + icon: IconHogQL, + inMenu: false, + }, + [NodeKind.SuggestedQuestionsQuery]: { + name: 'AI Suggested Questions', + icon: IconHogQL, + inMenu: false, + }, + [NodeKind.ActorsPropertyTaxonomyQuery]: { + name: 'Actor Property Taxonomy', + description: 'View the taxonomy of the actor’s property.', + icon: IconHogQL, + inMenu: false, + }, +} + +export const INSIGHT_TYPE_OPTIONS: LemonSelectOptions = [ + { value: 'All types', label: 'All types' }, + ...Object.entries(INSIGHT_TYPES_METADATA).map(([value, meta]) => ({ + value, + label: meta.name, + icon: meta.icon ? : undefined, + })), +] + +export const scene: SceneExport = { + component: AddSavedInsightsToDashboard, + logic: savedInsightsLogic, +} + +export function InsightIcon({ + insight, + className, +}: { + insight: QueryBasedInsightModel + className?: string +}): JSX.Element | null { + let Icon: (props?: any) => JSX.Element | null = () => null + + if ('query' in insight && isNonEmptyObject(insight.query)) { + const insightType = isNodeWithSource(insight.query) ? insight.query.source.kind : insight.query.kind + const insightMetadata = QUERY_TYPES_METADATA[insightType] + Icon = insightMetadata && insightMetadata.icon + } + + return Icon ? : null +} + +export function NewInsightButton({ dataAttr }: NewInsightButtonProps): JSX.Element { + return ( + } + > + New insight + + ) +} + +export function AddSavedInsightsToDashboard(): JSX.Element { + const { setSavedInsightsFilters, addInsightToDashboard, removeInsightFromDashboard } = + useActions(savedInsightsLogic) + const { insights, count, insightsLoading, filters, sorting, pagination, alertModalId } = + useValues(savedInsightsLogic) + const { hasTagging } = useValues(organizationLogic) + const { dashboard } = useValues(dashboardLogic) + + const summarizeInsight = useSummarizeInsight() + + const { tab, page } = filters + + const startCount = (page - 1) * INSIGHTS_PER_PAGE + 1 + const endCount = page * INSIGHTS_PER_PAGE < count ? page * INSIGHTS_PER_PAGE : count + + const columns: LemonTableColumns = [ + { + key: 'id', + width: 32, + render: function renderType(_, insight) { + return + }, + }, + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: function renderName(name: string, insight) { + return ( + <> + {name || {summarizeInsight(insight.query)}}} + description={insight.description} + /> + + ) + }, + }, + ...(hasTagging + ? [ + { + title: 'Tags', + dataIndex: 'tags' as keyof QueryBasedInsightModel, + key: 'tags', + render: function renderTags(tags: string[]) { + return + }, + }, + ] + : []), + ...(tab === SavedInsightsTabs.Yours + ? [] + : [ + createdByColumn() as LemonTableColumn< + QueryBasedInsightModel, + keyof QueryBasedInsightModel | undefined + >, + ]), + createdAtColumn() as LemonTableColumn, + { + title: 'Last modified', + sorter: true, + dataIndex: 'last_modified_at', + render: function renderLastModified(last_modified_at: string) { + return ( +
{last_modified_at && }
+ ) + }, + }, + { + width: 0, + render: function Render(_, insight) { + const isInDashboard = dashboard?.tiles.some((tile) => tile.insight?.id === insight.id) + return isInDashboard ? ( + { + removeInsightFromDashboard(insight, dashboard?.id || 0) + }} + data-attr="remove-insight-from-dashboard" + fullWidth + size="small" + type="primary" + icon={} + /> + ) : ( + { + addInsightToDashboard(insight, dashboard?.id || 0) + }} + data-attr="add-insight-to-dashboard" + fullWidth + size="small" + type="primary" + icon={} + /> + ) + }, + }, + ] + + return ( +
+ {tab === SavedInsightsTabs.History ? ( + + ) : tab === SavedInsightsTabs.Alerts ? ( + + ) : ( + <> + + +
+ + {count + ? `${startCount}${endCount - startCount > 1 ? '-' + endCount : ''} of ${count} insight${ + count === 1 ? '' : 's' + }` + : null} + +
+ {!insightsLoading && insights.count < 1 ? ( + + ) : ( + <> + + setSavedInsightsFilters({ + order: newSorting + ? `${newSorting.order === -1 ? '-' : ''}${newSorting.columnKey}` + : undefined, + }) + } + rowKey="id" + loadingSkeletonRows={15} + nouns={['insight', 'insights']} + /> + + )} + + )} +
+ ) +} diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index 1b958165f4f3b..448fcf27e9ee0 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -9,6 +9,7 @@ import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { PaginationManual } from 'lib/lemon-ui/PaginationControl' import { objectDiffShallow, objectsEqual, toParams } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' import { insightsApi } from 'scenes/insights/utils/api' @@ -88,6 +89,11 @@ export const savedInsightsLogic = kea([ addInsight: (insight: QueryBasedInsightModel) => ({ insight }), openAlertModal: (alertId: AlertType['id']) => ({ alertId }), closeAlertModal: true, + addInsightToDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ insight, dashboardId }), + removeInsightFromDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ + insight, + dashboardId, + }), }), loaders(({ values }) => ({ insights: { @@ -294,6 +300,33 @@ export const savedInsightsLogic = kea([ setDates: () => { actions.loadInsights() }, + addInsightToDashboard: async ({ insight, dashboardId }) => { + const response = await insightsApi.update(insight.id, { + dashboards: [...(insight.dashboards || []), dashboardId], + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight added to dashboard') + } + }, + removeInsightFromDashboard: async ({ insight, dashboardId }) => { + const response = await insightsApi.update(insight.id, { + dashboards: (insight.dashboards || []).filter((d) => d !== dashboardId), + dashboard_tiles: (insight.dashboard_tiles || []).filter((dt) => dt.dashboard_id !== dashboardId), + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight removed from dashboard') + } + }, [insightsModel.actionTypes.renameInsightSuccess]: ({ item }) => { actions.updateInsight(item) }, From fef85451e948469fda19000bcd000df67a14c412 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Mon, 30 Dec 2024 10:35:26 -0800 Subject: [PATCH 02/11] fix: cypress dashbaiord tests accomodate new modal --- cypress/productAnalytics/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/productAnalytics/index.ts b/cypress/productAnalytics/index.ts index 5bfdeae781326..7949e7eced1e9 100644 --- a/cypress/productAnalytics/index.ts +++ b/cypress/productAnalytics/index.ts @@ -167,6 +167,7 @@ export const dashboard = { cy.get('[data-attr=dashboard-add-graph-header]').contains('Add insight').click() cy.get('[data-attr=toast-close-button]').click({ multiple: true }) + cy.get('[data-attr=dashboard-add-new-insight]').contains('New insight').click() if (insightName) { cy.get('[data-attr="top-bar-name"] button').click() From c88a34c3665849a1a00fb13fc9be30defcfbcb7a Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Mon, 30 Dec 2024 10:36:49 -0800 Subject: [PATCH 03/11] --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e4a1521f8dd7a..5c1126f106830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -374,7 +374,7 @@ langsmith==0.1.132 # via # langchain # langchain-core -lxml==4.9.4 +lxml==5.2.2 # via # -r requirements.in # python3-saml @@ -807,7 +807,7 @@ wrapt==1.15.0 # langfuse wsproto==1.2.0 # via trio-websocket -xmlsec==1.3.13 +xmlsec==1.3.14 # via # -r requirements.in # python3-saml From f99f6996fb34c00d020c86fd2bd211d0dcc2dc3e Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Mon, 30 Dec 2024 10:40:16 -0800 Subject: [PATCH 04/11] fix: revert req change --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index bd26519927f97..ec758cf47f4ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -371,7 +371,7 @@ langsmith==0.1.132 # via # langchain # langchain-core -lxml==5.2.2 +lxml==4.9.4 # via # -r requirements.in # python3-saml @@ -803,7 +803,7 @@ wrapt==1.15.0 # langfuse wsproto==1.2.0 # via trio-websocket -xmlsec==1.3.14 +xmlsec==1.3.13 # via # -r requirements.in # python3-saml From 2454dc096d5edb98d7d59628890887fe4a1d918c Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Fri, 3 Jan 2025 15:53:22 -0800 Subject: [PATCH 05/11] feat: add pagination, dry up code and add loading state --- .../dashboard/AddInsightToDashboardModal.tsx | 1 - .../AddSavedInsightsToDashboard.tsx | 404 ++---------------- .../addSavedInsightsModalLogic.ts | 30 ++ .../saved-insights/savedInsightsLogic.ts | 63 ++- 4 files changed, 112 insertions(+), 386 deletions(-) create mode 100644 frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts diff --git a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx index 62c32ddad4db9..fd2a8a9cb58dd 100644 --- a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx @@ -31,7 +31,6 @@ export function AddInsightToDashboardModal(): JSX.Element { } > - {/*

Add insight to dashboard {dashboard?.name}

*/} ) diff --git a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx index 8684655cdb4a6..b711b596b3511 100644 --- a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx +++ b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx @@ -1,32 +1,11 @@ import './SavedInsights.scss' -import { - IconBrackets, - IconCorrelationAnalysis, - IconCursor, - IconFlask, - IconFunnels, - IconGraph, - IconHogQL, - IconLifecycle, - IconMinusSmall, - IconPerson, - IconPieChart, - IconPlusSmall, - IconRetention, - IconStickiness, - IconTrends, - IconUserPaths, - IconVideoCamera, - IconWarning, -} from '@posthog/icons' -import { LemonSelectOptions } from '@posthog/lemon-ui' +import { IconMinusSmall, IconPlusSmall } from '@posthog/icons' import { useActions, useValues } from 'kea' import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' import { Alerts } from 'lib/components/Alerts/views/Alerts' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { TZLabel } from 'lib/components/TZLabel' -import { IconAction, IconTableChart } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' @@ -42,328 +21,18 @@ import { SavedInsightsFilters } from 'scenes/saved-insights/SavedInsightsFilters import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { NodeKind } from '~/queries/schema' import { isNodeWithSource } from '~/queries/utils' -import { ActivityScope, InsightType, QueryBasedInsightModel, SavedInsightsTabs } from '~/types' +import { ActivityScope, QueryBasedInsightModel, SavedInsightsTabs } from '~/types' -import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' +import { addSavedInsightsModalLogic } from './addSavedInsightsModalLogic' +import { QUERY_TYPES_METADATA } from './SavedInsights' +import { savedInsightsLogic } from './savedInsightsLogic' interface NewInsightButtonProps { dataAttr: string } -export interface InsightTypeMetadata { - name: string - description?: string - icon: (props?: any) => JSX.Element | null - inMenu: boolean -} - -export const INSIGHT_TYPES_METADATA: Record = { - [InsightType.TRENDS]: { - name: 'Trends', - description: 'Visualize and break down how actions or events vary over time.', - icon: IconTrends, - inMenu: true, - }, - [InsightType.FUNNELS]: { - name: 'Funnel', - description: 'Discover how many users complete or drop out of a sequence of actions.', - icon: IconFunnels, - inMenu: true, - }, - [InsightType.RETENTION]: { - name: 'Retention', - description: 'See how many users return on subsequent days after an initial action.', - icon: IconRetention, - inMenu: true, - }, - [InsightType.PATHS]: { - name: 'Paths', - description: 'Trace the journeys users take within your product and where they drop off.', - icon: IconUserPaths, - inMenu: true, - }, - [InsightType.STICKINESS]: { - name: 'Stickiness', - description: 'See what keeps users coming back by viewing the interval between repeated actions.', - icon: IconStickiness, - inMenu: true, - }, - [InsightType.LIFECYCLE]: { - name: 'Lifecycle', - description: 'Understand growth by breaking down new, resurrected, returning and dormant users.', - icon: IconLifecycle, - inMenu: true, - }, - [InsightType.SQL]: { - name: 'SQL', - description: 'Use HogQL to query your data.', - icon: IconHogQL, - inMenu: true, - }, - [InsightType.JSON]: { - name: 'Custom', - description: 'Save components powered by our JSON query language.', - icon: IconBrackets, - inMenu: true, - }, - [InsightType.HOG]: { - name: 'Hog', - description: 'Use Hog to query your data.', - icon: IconHogQL, - inMenu: true, - }, -} - -export const QUERY_TYPES_METADATA: Record = { - [NodeKind.TrendsQuery]: { - name: 'Trends', - description: 'Visualize and break down how actions or events vary over time', - icon: IconTrends, - inMenu: true, - }, - [NodeKind.FunnelsQuery]: { - name: 'Funnel', - description: 'Discover how many users complete or drop out of a sequence of actions', - icon: IconFunnels, - inMenu: true, - }, - [NodeKind.RetentionQuery]: { - name: 'Retention', - description: 'See how many users return on subsequent days after an initial action', - icon: IconRetention, - inMenu: true, - }, - [NodeKind.PathsQuery]: { - name: 'Paths', - description: 'Trace the journeys users take within your product and where they drop off', - icon: IconUserPaths, - inMenu: true, - }, - [NodeKind.StickinessQuery]: { - name: 'Stickiness', - description: 'See what keeps users coming back by viewing the interval between repeated actions', - icon: IconStickiness, - inMenu: true, - }, - [NodeKind.LifecycleQuery]: { - name: 'Lifecycle', - description: 'Understand growth by breaking down new, resurrected, returning and dormant users', - icon: IconLifecycle, - inMenu: true, - }, - [NodeKind.FunnelCorrelationQuery]: { - name: 'Funnel Correlation', - description: 'See which events or properties correlate to a funnel result', - icon: IconCorrelationAnalysis, - inMenu: false, - }, - [NodeKind.EventsNode]: { - name: 'Events', - description: 'List and explore events', - icon: IconCursor, - inMenu: true, - }, - [NodeKind.ActionsNode]: { - name: 'Actions', - description: 'List and explore actions', - icon: IconAction, - inMenu: true, - }, - [NodeKind.DataWarehouseNode]: { - name: 'Data Warehouse', - description: 'List and explore data warehouse tables', - icon: IconTableChart, - inMenu: true, - }, - [NodeKind.EventsQuery]: { - name: 'Events Query', - description: 'List and explore events', - icon: IconCursor, - inMenu: true, - }, - [NodeKind.PersonsNode]: { - name: 'Persons', - description: 'List and explore your persons', - icon: IconPerson, - inMenu: true, - }, - [NodeKind.ActorsQuery]: { - name: 'Persons', - description: 'List of persons matching specified conditions', - icon: IconPerson, - inMenu: false, - }, - [NodeKind.InsightActorsQuery]: { - name: 'Persons', - description: 'List of persons matching specified conditions, derived from an insight', - icon: IconPerson, - inMenu: false, - }, - [NodeKind.InsightActorsQueryOptions]: { - name: 'Persons', - description: 'Options for InsightActorsQueryt', - icon: IconPerson, - inMenu: false, - }, - [NodeKind.FunnelsActorsQuery]: { - name: 'Persons', - description: 'List of persons matching specified conditions, derived from an insight', - icon: IconPerson, - inMenu: false, - }, - [NodeKind.FunnelCorrelationActorsQuery]: { - name: 'Persons', - description: 'List of persons matching specified conditions, derived from an insight', - icon: IconPerson, - inMenu: false, - }, - [NodeKind.DataTableNode]: { - name: 'Data table', - description: 'Slice and dice your data in a table', - icon: IconTableChart, - inMenu: true, - }, - [NodeKind.DataVisualizationNode]: { - name: 'Data visualization', - description: 'Slice and dice your data in a table or chart', - icon: IconTableChart, - inMenu: false, - }, - [NodeKind.SavedInsightNode]: { - name: 'Insight visualization by short id', - description: 'View your insights', - icon: IconGraph, - inMenu: true, - }, - [NodeKind.InsightVizNode]: { - name: 'Insight visualization', - description: 'View your insights', - icon: IconGraph, - inMenu: true, - }, - [NodeKind.SessionsTimelineQuery]: { - name: 'Sessions', - description: 'Sessions timeline query', - icon: IconTrends, - inMenu: true, - }, - [NodeKind.HogQLQuery]: { - name: 'HogQL', - description: 'Direct HogQL query', - icon: IconBrackets, - inMenu: true, - }, - [NodeKind.HogQLMetadata]: { - name: 'HogQL Metadata', - description: 'Metadata for a HogQL query', - icon: IconHogQL, - inMenu: true, - }, - [NodeKind.HogQLAutocomplete]: { - name: 'HogQL Autocomplete', - description: 'Autocomplete for the HogQL query editor', - icon: IconHogQL, - inMenu: false, - }, - [NodeKind.DatabaseSchemaQuery]: { - name: 'Database Schema', - description: 'Introspect the PostHog database schema', - icon: IconHogQL, - inMenu: true, - }, - [NodeKind.WebOverviewQuery]: { - name: 'Overview Stats', - description: 'View overview stats for a website', - icon: IconPieChart, - inMenu: true, - }, - [NodeKind.WebStatsTableQuery]: { - name: 'Web Table', - description: 'A table of results from web analytics, with a breakdown', - icon: IconPieChart, - inMenu: true, - }, - [NodeKind.WebGoalsQuery]: { - name: 'Goals', - description: 'View goal conversions', - icon: IconPieChart, - inMenu: true, - }, - [NodeKind.WebExternalClicksTableQuery]: { - name: 'External click urls', - description: 'View clicks on external links', - icon: IconPieChart, - inMenu: true, - }, - [NodeKind.HogQuery]: { - name: 'Hog', - description: 'Hog query', - icon: IconHogQL, - inMenu: true, - }, - [NodeKind.SessionAttributionExplorerQuery]: { - name: 'Session Attribution', - description: 'Session Attribution Explorer', - icon: IconPieChart, - inMenu: true, - }, - [NodeKind.ErrorTrackingQuery]: { - name: 'Error Tracking', - description: 'List and explore exception groups', - icon: IconWarning, - inMenu: false, - }, - [NodeKind.RecordingsQuery]: { - name: 'Session Recordings', - description: 'View available recordings', - icon: IconVideoCamera, - inMenu: false, - }, - [NodeKind.ExperimentTrendsQuery]: { - name: 'Experiment Trends Result', - description: 'View experiment trend result', - icon: IconFlask, - inMenu: false, - }, - [NodeKind.ExperimentFunnelsQuery]: { - name: 'Experiment Funnels Result', - description: 'View experiment funnel result', - icon: IconFlask, - inMenu: false, - }, - [NodeKind.TeamTaxonomyQuery]: { - name: 'Team Taxonomy', - icon: IconHogQL, - inMenu: false, - }, - [NodeKind.EventTaxonomyQuery]: { - name: 'Event Taxonomy', - icon: IconHogQL, - inMenu: false, - }, - [NodeKind.SuggestedQuestionsQuery]: { - name: 'AI Suggested Questions', - icon: IconHogQL, - inMenu: false, - }, - [NodeKind.ActorsPropertyTaxonomyQuery]: { - name: 'Actor Property Taxonomy', - description: 'View the taxonomy of the actor’s property.', - icon: IconHogQL, - inMenu: false, - }, -} - -export const INSIGHT_TYPE_OPTIONS: LemonSelectOptions = [ - { value: 'All types', label: 'All types' }, - ...Object.entries(INSIGHT_TYPES_METADATA).map(([value, meta]) => ({ - value, - label: meta.name, - icon: meta.icon ? : undefined, - })), -] +const INSIGHTS_PER_PAGE = 15 export const scene: SceneExport = { component: AddSavedInsightsToDashboard, @@ -412,19 +81,23 @@ export function NewInsightButton({ dataAttr }: NewInsightButtonProps): JSX.Eleme } export function AddSavedInsightsToDashboard(): JSX.Element { + const { modalPage } = useValues(addSavedInsightsModalLogic) + const { setModalPage } = useActions(addSavedInsightsModalLogic) + + const { insights, count, insightsLoading, filters, sorting, alertModalId, dashboardUpdatesInProgress } = + useValues(savedInsightsLogic) const { setSavedInsightsFilters, addInsightToDashboard, removeInsightFromDashboard } = useActions(savedInsightsLogic) - const { insights, count, insightsLoading, filters, sorting, pagination, alertModalId } = - useValues(savedInsightsLogic) + const { hasTagging } = useValues(organizationLogic) const { dashboard } = useValues(dashboardLogic) const summarizeInsight = useSummarizeInsight() - const { tab, page } = filters + const { tab } = filters - const startCount = (page - 1) * INSIGHTS_PER_PAGE + 1 - const endCount = page * INSIGHTS_PER_PAGE < count ? page * INSIGHTS_PER_PAGE : count + const startCount = (modalPage - 1) * INSIGHTS_PER_PAGE + 1 + const endCount = Math.min(modalPage * INSIGHTS_PER_PAGE, count) const columns: LemonTableColumns = [ { @@ -485,28 +158,25 @@ export function AddSavedInsightsToDashboard(): JSX.Element { width: 0, render: function Render(_, insight) { const isInDashboard = dashboard?.tiles.some((tile) => tile.insight?.id === insight.id) - return isInDashboard ? ( + return ( { - removeInsightFromDashboard(insight, dashboard?.id || 0) - }} - data-attr="remove-insight-from-dashboard" - fullWidth + type="secondary" + status={isInDashboard ? 'danger' : 'default'} + loading={dashboardUpdatesInProgress[insight.id]} size="small" - type="primary" - icon={} - /> - ) : ( - { - addInsightToDashboard(insight, dashboard?.id || 0) - }} - data-attr="add-insight-to-dashboard" fullWidth - size="small" - type="primary" - icon={} - /> + onClick={(e) => { + e.preventDefault() + if (dashboardUpdatesInProgress[insight.id]) { + return + } + isInDashboard + ? removeInsightFromDashboard(insight, dashboard?.id || 0) + : addInsightToDashboard(insight, dashboard?.id || 0) + }} + > + {isInDashboard ? : } + ) }, }, @@ -539,8 +209,14 @@ export function AddSavedInsightsToDashboard(): JSX.Element { loading={insightsLoading} columns={columns} dataSource={insights.results} - pagination={pagination} - noSortingCancellation + pagination={{ + controlled: true, + currentPage: modalPage, + pageSize: INSIGHTS_PER_PAGE, + entryCount: count, + onForward: () => setModalPage(modalPage + 1), + onBackward: () => setModalPage(modalPage - 1), + }} sorting={sorting} onSort={(newSorting) => setSavedInsightsFilters({ @@ -550,7 +226,7 @@ export function AddSavedInsightsToDashboard(): JSX.Element { }) } rowKey="id" - loadingSkeletonRows={15} + loadingSkeletonRows={INSIGHTS_PER_PAGE} nouns={['insight', 'insights']} /> diff --git a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts new file mode 100644 index 0000000000000..801a41a52986e --- /dev/null +++ b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts @@ -0,0 +1,30 @@ +import { kea } from 'kea' + +import type { addSavedInsightsModalLogicType } from './addSavedInsightsModalLogicType' +import { savedInsightsLogic } from './savedInsightsLogic' + +// This logic is used to control the pagination of the insights in the add saved insights to dashboard modal +export const addSavedInsightsModalLogic = kea({ + path: ['scenes', 'saved-insights', 'addSavedInsightsModalLogic'], + connect: { + logic: [savedInsightsLogic], + }, + actions: () => ({ + setModalPage: (page: number) => ({ page }), + }), + + reducers: () => ({ + modalPage: [ + 1, + { + setModalPage: (_, { page }) => page, + }, + ], + }), + + listeners: () => ({ + setModalPage: async ({ page }) => { + savedInsightsLogic.actions.setSavedInsightsFilters({ page }, true, false) + }, + }), +}) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index 448fcf27e9ee0..b19e572f80583 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -94,6 +94,7 @@ export const savedInsightsLogic = kea([ insight, dashboardId, }), + setDashboardUpdateLoading: (insightId: number, loading: boolean) => ({ insightId, loading }), }), loaders(({ values }) => ({ insights: { @@ -189,6 +190,14 @@ export const savedInsightsLogic = kea([ closeAlertModal: () => null, }, ], + dashboardUpdatesInProgress: [ + {} as Record, + { + setDashboardUpdateLoading: (state, { insightId, loading }) => { + return { ...state, [insightId]: loading } + }, + }, + ], }), selectors({ filters: [(s) => [s.rawFilters], (rawFilters): SavedInsightFilters => cleanFilters(rawFilters || {})], @@ -301,30 +310,42 @@ export const savedInsightsLogic = kea([ actions.loadInsights() }, addInsightToDashboard: async ({ insight, dashboardId }) => { - const response = await insightsApi.update(insight.id, { - dashboards: [...(insight.dashboards || []), dashboardId], - }) - if (response) { - actions.updateInsight(response) - const logic = dashboardLogic({ id: dashboardId }) - logic.mount() - logic.actions.loadDashboard({ action: 'update' }) - logic.unmount() - lemonToast.success('Insight added to dashboard') + try { + actions.setDashboardUpdateLoading(insight.id, true) + + const response = await insightsApi.update(insight.id, { + dashboards: [...(insight.dashboards || []), dashboardId], + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight added to dashboard') + } + } finally { + actions.setDashboardUpdateLoading(insight.id, false) } }, removeInsightFromDashboard: async ({ insight, dashboardId }) => { - const response = await insightsApi.update(insight.id, { - dashboards: (insight.dashboards || []).filter((d) => d !== dashboardId), - dashboard_tiles: (insight.dashboard_tiles || []).filter((dt) => dt.dashboard_id !== dashboardId), - }) - if (response) { - actions.updateInsight(response) - const logic = dashboardLogic({ id: dashboardId }) - logic.mount() - logic.actions.loadDashboard({ action: 'update' }) - logic.unmount() - lemonToast.success('Insight removed from dashboard') + try { + actions.setDashboardUpdateLoading(insight.id, true) + + const response = await insightsApi.update(insight.id, { + dashboards: (insight.dashboards || []).filter((d) => d !== dashboardId), + dashboard_tiles: (insight.dashboard_tiles || []).filter((dt) => dt.dashboard_id !== dashboardId), + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight removed from dashboard') + } + } finally { + actions.setDashboardUpdateLoading(insight.id, false) } }, [insightsModel.actionTypes.renameInsightSuccess]: ({ item }) => { From 687b20a75debc28c510ee8def103a18035ef985d Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jan 2025 13:32:32 -0800 Subject: [PATCH 06/11] feat: fix pagination and remove unused code from modal --- .../dashboard/AddInsightToDashboardModal.tsx | 2 +- .../src/scenes/dashboard/DashboardHeader.tsx | 2 +- .../dashboard/EmptyDashboardComponent.tsx | 2 +- ....ts => addInsightToDashboardModalLogic.ts} | 2 +- .../AddSavedInsightsToDashboard.tsx | 159 ++++++------------ .../saved-insights/savedInsightsLogic.ts | 4 + 6 files changed, 58 insertions(+), 113 deletions(-) rename frontend/src/scenes/dashboard/{addInsightToDasboardModalLogic.ts => addInsightToDashboardModalLogic.ts} (95%) diff --git a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx index fd2a8a9cb58dd..5acab131086cf 100644 --- a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { addInsightToDashboardLogic } from 'scenes/dashboard/addInsightToDasboardModalLogic' +import { addInsightToDashboardLogic } from 'scenes/dashboard/addInsightToDashboardModalLogic' import { AddSavedInsightsToDashboard } from 'scenes/saved-insights/AddSavedInsightsToDashboard' import { urls } from 'scenes/urls' diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 2539557188c67..8806e5e91cd24 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -28,8 +28,8 @@ import { notebooksModel } from '~/models/notebooksModel' import { tagsModel } from '~/models/tagsModel' import { DashboardMode, DashboardType, ExporterFormat, QueryBasedInsightModel } from '~/types' -import { addInsightToDashboardLogic } from './addInsightToDasboardModalLogic' import { AddInsightToDashboardModal } from './AddInsightToDashboardModal' +import { addInsightToDashboardLogic } from './addInsightToDashboardModalLogic' import { DASHBOARD_RESTRICTION_OPTIONS } from './DashboardCollaborators' import { dashboardCollaboratorsLogic } from './dashboardCollaboratorsLogic' import { dashboardLogic } from './dashboardLogic' diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx index 6558513ed8ef7..17856b7378e4b 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx @@ -6,7 +6,7 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import React from 'react' -import { addInsightToDashboardLogic } from './addInsightToDasboardModalLogic' +import { addInsightToDashboardLogic } from './addInsightToDashboardModalLogic' import { DASHBOARD_CANNOT_EDIT_MESSAGE } from './DashboardHeader' function SkeletonCard({ children, active }: { children: React.ReactNode; active: boolean }): JSX.Element { diff --git a/frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts b/frontend/src/scenes/dashboard/addInsightToDashboardModalLogic.ts similarity index 95% rename from frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts rename to frontend/src/scenes/dashboard/addInsightToDashboardModalLogic.ts index 2bf2b03a3e01f..bf5d82510bd6d 100644 --- a/frontend/src/scenes/dashboard/addInsightToDasboardModalLogic.ts +++ b/frontend/src/scenes/dashboard/addInsightToDashboardModalLogic.ts @@ -1,6 +1,6 @@ import { actions, kea, path, reducers } from 'kea' -import type { addInsightToDashboardLogicType } from './addInsightToDasboardModalLogicType' +import type { addInsightToDashboardLogicType } from './addInsightToDashboardModalLogicType' export const addInsightToDashboardLogic = kea([ path(['scenes', 'dashboard', 'addInsightToDashboardLogic']), diff --git a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx index b711b596b3511..ae8f4f32e85c4 100644 --- a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx +++ b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx @@ -2,8 +2,6 @@ import './SavedInsights.scss' import { IconMinusSmall, IconPlusSmall } from '@posthog/icons' import { useActions, useValues } from 'kea' -import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' -import { Alerts } from 'lib/components/Alerts/views/Alerts' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { TZLabel } from 'lib/components/TZLabel' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -11,80 +9,25 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { isNonEmptyObject } from 'lib/utils' +import { Spinner } from 'lib/lemon-ui/Spinner' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { SavedInsightsEmptyState } from 'scenes/insights/EmptyStates' import { useSummarizeInsight } from 'scenes/insights/summarizeInsight' import { organizationLogic } from 'scenes/organizationLogic' -import { overlayForNewInsightMenu } from 'scenes/saved-insights/newInsightsMenu' import { SavedInsightsFilters } from 'scenes/saved-insights/SavedInsightsFilters' -import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { isNodeWithSource } from '~/queries/utils' -import { ActivityScope, QueryBasedInsightModel, SavedInsightsTabs } from '~/types' +import { QueryBasedInsightModel, SavedInsightsTabs } from '~/types' import { addSavedInsightsModalLogic } from './addSavedInsightsModalLogic' -import { QUERY_TYPES_METADATA } from './SavedInsights' -import { savedInsightsLogic } from './savedInsightsLogic' - -interface NewInsightButtonProps { - dataAttr: string -} - -const INSIGHTS_PER_PAGE = 15 - -export const scene: SceneExport = { - component: AddSavedInsightsToDashboard, - logic: savedInsightsLogic, -} - -export function InsightIcon({ - insight, - className, -}: { - insight: QueryBasedInsightModel - className?: string -}): JSX.Element | null { - let Icon: (props?: any) => JSX.Element | null = () => null - - if ('query' in insight && isNonEmptyObject(insight.query)) { - const insightType = isNodeWithSource(insight.query) ? insight.query.source.kind : insight.query.kind - const insightMetadata = QUERY_TYPES_METADATA[insightType] - Icon = insightMetadata && insightMetadata.icon - } - - return Icon ? : null -} - -export function NewInsightButton({ dataAttr }: NewInsightButtonProps): JSX.Element { - return ( - } - > - New insight - - ) -} +import { InsightIcon } from './SavedInsights' +import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' export function AddSavedInsightsToDashboard(): JSX.Element { const { modalPage } = useValues(addSavedInsightsModalLogic) const { setModalPage } = useActions(addSavedInsightsModalLogic) - const { insights, count, insightsLoading, filters, sorting, alertModalId, dashboardUpdatesInProgress } = + const { insights, count, insightsLoading, filters, sorting, dashboardUpdatesInProgress } = useValues(savedInsightsLogic) const { setSavedInsightsFilters, addInsightToDashboard, removeInsightFromDashboard } = useActions(savedInsightsLogic) @@ -162,9 +105,9 @@ export function AddSavedInsightsToDashboard(): JSX.Element { { e.preventDefault() if (dashboardUpdatesInProgress[insight.id]) { @@ -175,7 +118,13 @@ export function AddSavedInsightsToDashboard(): JSX.Element { : addInsightToDashboard(insight, dashboard?.id || 0) }} > - {isInDashboard ? : } + {dashboardUpdatesInProgress[insight.id] ? ( + + ) : isInDashboard ? ( + + ) : ( + + )} ) }, @@ -184,53 +133,45 @@ export function AddSavedInsightsToDashboard(): JSX.Element { return (
- {tab === SavedInsightsTabs.History ? ( - - ) : tab === SavedInsightsTabs.Alerts ? ( - + + +
+ + {count + ? `${startCount}${endCount - startCount > 1 ? '-' + endCount : ''} of ${count} insight${ + count === 1 ? '' : 's' + }` + : null} + +
+ {!insightsLoading && insights.count < 1 ? ( + ) : ( <> - - -
- - {count - ? `${startCount}${endCount - startCount > 1 ? '-' + endCount : ''} of ${count} insight${ - count === 1 ? '' : 's' - }` - : null} - -
- {!insightsLoading && insights.count < 1 ? ( - - ) : ( - <> - setModalPage(modalPage + 1), - onBackward: () => setModalPage(modalPage - 1), - }} - sorting={sorting} - onSort={(newSorting) => - setSavedInsightsFilters({ - order: newSorting - ? `${newSorting.order === -1 ? '-' : ''}${newSorting.columnKey}` - : undefined, - }) - } - rowKey="id" - loadingSkeletonRows={INSIGHTS_PER_PAGE} - nouns={['insight', 'insights']} - /> - - )} + setModalPage(modalPage + 1), + onBackward: () => setModalPage(modalPage - 1), + }} + sorting={sorting} + onSort={(newSorting) => + setSavedInsightsFilters({ + order: newSorting + ? `${newSorting.order === -1 ? '-' : ''}${newSorting.columnKey}` + : undefined, + }) + } + rowKey="id" + loadingSkeletonRows={INSIGHTS_PER_PAGE} + nouns={['insight', 'insights']} + /> )}
diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index b19e572f80583..e5a9bf7a4c44a 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -406,6 +406,10 @@ export const savedInsightsLogic = kea([ { alert_id, ...searchParams }, // search params, hashParams ) => { + // Add scene check + if (sceneLogic.findMounted()?.values.activeScene !== Scene.SavedInsights) { + return + } if (alert_id) { actions.openAlertModal(alert_id) } else { From 8f3a01036c1bb9ab6f4976277bb101d138365c04 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jan 2025 15:43:46 -0800 Subject: [PATCH 07/11] feat: seperate kea logic from saved insights table --- .../dashboard/AddInsightToDashboardModal.tsx | 52 +++-- .../AddSavedInsightsToDashboard.tsx | 18 +- .../scenes/saved-insights/SavedInsights.tsx | 2 +- .../saved-insights/SavedInsightsFilters.tsx | 26 ++- .../addSavedInsightsModalLogic.ts | 207 ++++++++++++++++-- .../saved-insights/savedInsightsLogic.ts | 3 +- 6 files changed, 241 insertions(+), 67 deletions(-) diff --git a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx index 5acab131086cf..3375377710b01 100644 --- a/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx +++ b/frontend/src/scenes/dashboard/AddInsightToDashboardModal.tsx @@ -1,10 +1,12 @@ import { useActions, useValues } from 'kea' +import { BindLogic } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { addInsightToDashboardLogic } from 'scenes/dashboard/addInsightToDashboardModalLogic' +import { addSavedInsightsModalLogic } from 'scenes/saved-insights/addSavedInsightsModalLogic' import { AddSavedInsightsToDashboard } from 'scenes/saved-insights/AddSavedInsightsToDashboard' import { urls } from 'scenes/urls' +import { addInsightToDashboardLogic } from './addInsightToDashboardModalLogic' import { dashboardLogic } from './dashboardLogic' export function AddInsightToDashboardModal(): JSX.Element { @@ -12,26 +14,32 @@ export function AddInsightToDashboardModal(): JSX.Element { const { addInsightToDashboardModalVisible } = useValues(addInsightToDashboardLogic) const { dashboard } = useValues(dashboardLogic) return ( - - - Cancel - - - New insight - - - } - > - - + + + + Cancel + + + New insight + + + } + > + + + ) } diff --git a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx index ae8f4f32e85c4..6f95bb4ed7039 100644 --- a/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx +++ b/frontend/src/scenes/saved-insights/AddSavedInsightsToDashboard.tsx @@ -19,18 +19,14 @@ import { urls } from 'scenes/urls' import { QueryBasedInsightModel, SavedInsightsTabs } from '~/types' -import { addSavedInsightsModalLogic } from './addSavedInsightsModalLogic' +import { addSavedInsightsModalLogic, INSIGHTS_PER_PAGE } from './addSavedInsightsModalLogic' import { InsightIcon } from './SavedInsights' -import { INSIGHTS_PER_PAGE, savedInsightsLogic } from './savedInsightsLogic' export function AddSavedInsightsToDashboard(): JSX.Element { - const { modalPage } = useValues(addSavedInsightsModalLogic) - const { setModalPage } = useActions(addSavedInsightsModalLogic) - - const { insights, count, insightsLoading, filters, sorting, dashboardUpdatesInProgress } = - useValues(savedInsightsLogic) - const { setSavedInsightsFilters, addInsightToDashboard, removeInsightFromDashboard } = - useActions(savedInsightsLogic) + const { modalPage, insights, count, insightsLoading, filters, sorting, dashboardUpdatesInProgress } = + useValues(addSavedInsightsModalLogic) + const { setModalPage, addInsightToDashboard, removeInsightFromDashboard, setModalFilters } = + useActions(addSavedInsightsModalLogic) const { hasTagging } = useValues(organizationLogic) const { dashboard } = useValues(dashboardLogic) @@ -133,7 +129,7 @@ export function AddSavedInsightsToDashboard(): JSX.Element { return (
- +
@@ -162,7 +158,7 @@ export function AddSavedInsightsToDashboard(): JSX.Element { }} sorting={sorting} onSort={(newSorting) => - setSavedInsightsFilters({ + setModalFilters({ order: newSorting ? `${newSorting.order === -1 ? '-' : ''}${newSorting.columnKey}` : undefined, diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index bd155048b0490..83b3c17daa741 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -638,7 +638,7 @@ export function SavedInsights(): JSX.Element { ) : ( <> - +
diff --git a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx index c8d5351301553..a3c3607e36755 100644 --- a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx @@ -1,19 +1,23 @@ import { IconCalendar } from '@posthog/icons' -import { useActions, useValues } from 'kea' +import { useValues } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { MemberSelect } from 'lib/components/MemberSelect' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { INSIGHT_TYPE_OPTIONS } from 'scenes/saved-insights/SavedInsights' -import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' +import { SavedInsightFilters } from 'scenes/saved-insights/savedInsightsLogic' import { dashboardsModel } from '~/models/dashboardsModel' import { SavedInsightsTabs } from '~/types' -export function SavedInsightsFilters(): JSX.Element { +export function SavedInsightsFilters({ + filters, + setFilters, +}: { + filters: SavedInsightFilters + setFilters: (filters: Partial) => void +}): JSX.Element { const { nameSortedDashboards } = useValues(dashboardsModel) - const { setSavedInsightsFilters } = useActions(savedInsightsLogic) - const { filters } = useValues(savedInsightsLogic) const { tab, createdBy, insightType, dateFrom, dateTo, dashboardId, search } = filters @@ -22,7 +26,7 @@ export function SavedInsightsFilters(): JSX.Element { setSavedInsightsFilters({ search: value })} + onChange={(value) => setFilters({ search: value })} value={search || ''} />
@@ -37,7 +41,7 @@ export function SavedInsightsFilters(): JSX.Element { }))} value={dashboardId} onChange={(newValue) => { - setSavedInsightsFilters({ dashboardId: newValue }) + setFilters({ dashboardId: newValue }) }} dropdownMatchSelectWidth={false} data-attr="insight-on-dashboard" @@ -51,7 +55,7 @@ export function SavedInsightsFilters(): JSX.Element { size="small" options={INSIGHT_TYPE_OPTIONS} value={insightType} - onChange={(v: any): void => setSavedInsightsFilters({ insightType: v })} + onChange={(v: any): void => setFilters({ insightType: v })} dropdownMatchSelectWidth={false} data-attr="insight-type" /> @@ -62,9 +66,7 @@ export function SavedInsightsFilters(): JSX.Element { disabled={false} dateFrom={dateFrom} dateTo={dateTo} - onChange={(fromDate, toDate) => - setSavedInsightsFilters({ dateFrom: fromDate, dateTo: toDate ?? undefined }) - } + onChange={(fromDate, toDate) => setFilters({ dateFrom: fromDate, dateTo: toDate ?? undefined })} makeLabel={(key) => ( <> @@ -78,7 +80,7 @@ export function SavedInsightsFilters(): JSX.Element { Created by: setSavedInsightsFilters({ createdBy: user?.id || 'All users' })} + onChange={(user) => setFilters({ createdBy: user?.id || 'All users' })} />
) : null} diff --git a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts index 801a41a52986e..93026adb15110 100644 --- a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts +++ b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts @@ -1,30 +1,199 @@ -import { kea } from 'kea' +import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { Sorting } from 'lib/lemon-ui/LemonTable' +import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' +import { objectsEqual, toParams } from 'lib/utils' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' +import { insightsApi } from 'scenes/insights/utils/api' +import { teamLogic } from 'scenes/teamLogic' + +import { getQueryBasedInsightModel } from '~/queries/nodes/InsightViz/utils' +import type { QueryBasedInsightModel } from '~/types' import type { addSavedInsightsModalLogicType } from './addSavedInsightsModalLogicType' -import { savedInsightsLogic } from './savedInsightsLogic' - -// This logic is used to control the pagination of the insights in the add saved insights to dashboard modal -export const addSavedInsightsModalLogic = kea({ - path: ['scenes', 'saved-insights', 'addSavedInsightsModalLogic'], - connect: { - logic: [savedInsightsLogic], - }, - actions: () => ({ +import { cleanFilters, SavedInsightFilters } from './savedInsightsLogic' + +// Page size for insights loaded into the modal +export const INSIGHTS_PER_PAGE = 30 + +export const addSavedInsightsModalLogic = kea([ + path(['scenes', 'saved-insights', 'addSavedInsightsModalLogic']), + connect({ + values: [teamLogic, ['currentTeamId']], + logic: [eventUsageLogic], + }), + actions({ + // Basic filtering & pagination + setModalFilters: (filters: Partial, merge: boolean = true, debounce: boolean = true) => ({ + filters, + merge, + debounce, + }), + loadInsights: true, setModalPage: (page: number) => ({ page }), + + // Dashboard updates + setDashboardUpdateLoading: (insightId: number, loading: boolean) => ({ insightId, loading }), + addInsightToDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ insight, dashboardId }), + removeInsightFromDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ + insight, + dashboardId, + }), + + // Update locally in this store + updateInsight: (insight: QueryBasedInsightModel) => ({ insight }), }), + loaders(({ values }) => ({ + insights: { + __default: { results: [] as QueryBasedInsightModel[], count: 0 }, + loadInsights: async () => { + const { order, page, search, dashboardId, insightType, createdBy, dateFrom, dateTo } = values.filters + + const params: Record = { + order, + limit: INSIGHTS_PER_PAGE, + offset: Math.max(0, (page - 1) * INSIGHTS_PER_PAGE), + saved: true, + } - reducers: () => ({ - modalPage: [ - 1, + if (search) { + params.search = search + } + if (insightType && insightType.toLowerCase() !== 'all types') { + params.insight = insightType.toUpperCase() + } + if (createdBy && createdBy !== 'All users') { + params.created_by = createdBy + } + if (dateFrom && dateFrom !== 'all') { + params.date_from = dateFrom + params.date_to = dateTo || undefined + } + if (dashboardId) { + params.dashboards = [dashboardId] + } + + const response = await api.get( + `api/environments/${teamLogic.values.currentTeamId}/insights/?${toParams(params)}` + ) + + return { + ...response, + results: response.results.map((rawInsight: any) => getQueryBasedInsightModel(rawInsight)), + } + }, + }, + })), + reducers({ + // Keep our filters separate from the ones on the main page + rawModalFilters: [ + null as Partial | null, + { + setModalFilters: (state, { filters, merge }) => { + return cleanFilters({ + ...(merge ? state || {} : {}), + ...filters, + // If we update search/order, reset to page=1 + ...('page' in filters ? {} : { page: 1 }), + }) + }, + }, + ], + insights: { + updateInsight: (state, { insight }) => ({ + ...state, + results: state.results.map((i) => (i.short_id === insight.short_id ? insight : i)), + }), + }, + dashboardUpdatesInProgress: [ + {} as Record, { - setModalPage: (_, { page }) => page, + setDashboardUpdateLoading: (state, { insightId, loading }) => ({ + ...state, + [insightId]: loading, + }), }, ], }), - - listeners: () => ({ + selectors({ + filters: [ + (s) => [s.rawModalFilters], + (rawModalFilters): SavedInsightFilters => cleanFilters(rawModalFilters || {}), + ], + count: [(s) => [s.insights], (insights) => insights.count], + // For a standard table sorting object + sorting: [ + (s) => [s.filters], + (filters): Sorting | null => + filters.order + ? filters.order.startsWith('-') + ? { columnKey: filters.order.slice(1), order: -1 } + : { columnKey: filters.order, order: 1 } + : null, + ], + modalPage: [(s) => [s.filters], (filters) => filters.page], + }), + listeners(({ actions, values, selectors }) => ({ + // Trigger load when changing page manually setModalPage: async ({ page }) => { - savedInsightsLogic.actions.setSavedInsightsFilters({ page }, true, false) + actions.setModalFilters({ page }, true) + actions.loadInsights() }, - }), -}) + setModalFilters: async ({ debounce }, breakpoint, __, previousState) => { + const oldFilters = selectors.filters(previousState) + const newFilters = values.filters + + if (debounce) { + await breakpoint(300) + } + if (!objectsEqual(oldFilters, newFilters)) { + actions.loadInsights() + } + }, + + addInsightToDashboard: async ({ insight, dashboardId }) => { + try { + actions.setDashboardUpdateLoading(insight.id, true) + const response = await insightsApi.update(insight.id, { + dashboards: [...(insight.dashboards || []), dashboardId], + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight added to dashboard') + } + } finally { + actions.setDashboardUpdateLoading(insight.id, false) + } + }, + removeInsightFromDashboard: async ({ insight, dashboardId }) => { + try { + actions.setDashboardUpdateLoading(insight.id, true) + const response = await insightsApi.update(insight.id, { + dashboards: (insight.dashboards || []).filter((d) => d !== dashboardId), + dashboard_tiles: (insight.dashboard_tiles || []).filter((dt) => dt.dashboard_id !== dashboardId), + }) + if (response) { + actions.updateInsight(response) + const logic = dashboardLogic({ id: dashboardId }) + logic.mount() + logic.actions.loadDashboard({ action: 'update' }) + logic.unmount() + lemonToast.success('Insight removed from dashboard') + } + } finally { + actions.setDashboardUpdateLoading(insight.id, false) + } + }, + })), + events(({ actions }) => ({ + afterMount: () => { + actions.loadInsights() + }, + })), +]) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index e5a9bf7a4c44a..bc8d7340bf842 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -51,7 +51,7 @@ export interface SavedInsightFilters { dashboardId: number | undefined | null } -function cleanFilters(values: Partial): SavedInsightFilters { +export function cleanFilters(values: Partial): SavedInsightFilters { return { layoutView: values.layoutView || LayoutView.List, order: values.order || '-last_modified_at', // Sync with `sorting` selector @@ -406,7 +406,6 @@ export const savedInsightsLogic = kea([ { alert_id, ...searchParams }, // search params, hashParams ) => { - // Add scene check if (sceneLogic.findMounted()?.values.activeScene !== Scene.SavedInsights) { return } From 902d8c8e499c98768b07629328c6ae24450c5d27 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jan 2025 15:52:29 -0800 Subject: [PATCH 08/11] fix: cleanup some comments and nits --- .../src/scenes/dashboard/DashboardHeader.tsx | 4 +- .../dashboard/EmptyDashboardComponent.tsx | 4 +- .../addSavedInsightsModalLogic.ts | 8 ---- .../saved-insights/savedInsightsLogic.ts | 45 ------------------- 4 files changed, 2 insertions(+), 59 deletions(-) diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 8806e5e91cd24..e01d8598762e2 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -293,9 +293,7 @@ export function DashboardHeader(): JSX.Element | null { )} {dashboard ? ( { - showAddInsightToDashboardModal() - }} + onClick={showAddInsightToDashboardModal} type="primary" data-attr="dashboard-add-graph-header" disabledReason={canEditDashboard ? null : DASHBOARD_CANNOT_EDIT_MESSAGE} diff --git a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx index 17856b7378e4b..0e3d5b809dc32 100644 --- a/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx +++ b/frontend/src/scenes/dashboard/EmptyDashboardComponent.tsx @@ -86,9 +86,7 @@ export function EmptyDashboardComponent({ loading, canEdit }: { loading: boolean
{ - showAddInsightToDashboardModal() - }} + onClick={showAddInsightToDashboardModal} type="primary" icon={} center diff --git a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts index 93026adb15110..0235c5e022300 100644 --- a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts +++ b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts @@ -15,7 +15,6 @@ import type { QueryBasedInsightModel } from '~/types' import type { addSavedInsightsModalLogicType } from './addSavedInsightsModalLogicType' import { cleanFilters, SavedInsightFilters } from './savedInsightsLogic' -// Page size for insights loaded into the modal export const INSIGHTS_PER_PAGE = 30 export const addSavedInsightsModalLogic = kea([ @@ -25,7 +24,6 @@ export const addSavedInsightsModalLogic = kea([ logic: [eventUsageLogic], }), actions({ - // Basic filtering & pagination setModalFilters: (filters: Partial, merge: boolean = true, debounce: boolean = true) => ({ filters, merge, @@ -34,7 +32,6 @@ export const addSavedInsightsModalLogic = kea([ loadInsights: true, setModalPage: (page: number) => ({ page }), - // Dashboard updates setDashboardUpdateLoading: (insightId: number, loading: boolean) => ({ insightId, loading }), addInsightToDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ insight, dashboardId }), removeInsightFromDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ @@ -42,7 +39,6 @@ export const addSavedInsightsModalLogic = kea([ dashboardId, }), - // Update locally in this store updateInsight: (insight: QueryBasedInsightModel) => ({ insight }), }), loaders(({ values }) => ({ @@ -87,7 +83,6 @@ export const addSavedInsightsModalLogic = kea([ }, })), reducers({ - // Keep our filters separate from the ones on the main page rawModalFilters: [ null as Partial | null, { @@ -95,7 +90,6 @@ export const addSavedInsightsModalLogic = kea([ return cleanFilters({ ...(merge ? state || {} : {}), ...filters, - // If we update search/order, reset to page=1 ...('page' in filters ? {} : { page: 1 }), }) }, @@ -123,7 +117,6 @@ export const addSavedInsightsModalLogic = kea([ (rawModalFilters): SavedInsightFilters => cleanFilters(rawModalFilters || {}), ], count: [(s) => [s.insights], (insights) => insights.count], - // For a standard table sorting object sorting: [ (s) => [s.filters], (filters): Sorting | null => @@ -136,7 +129,6 @@ export const addSavedInsightsModalLogic = kea([ modalPage: [(s) => [s.filters], (filters) => filters.page], }), listeners(({ actions, values, selectors }) => ({ - // Trigger load when changing page manually setModalPage: async ({ page }) => { actions.setModalFilters({ page }, true) actions.loadInsights() diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index bc8d7340bf842..d91589bd50135 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -9,7 +9,6 @@ import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { PaginationManual } from 'lib/lemon-ui/PaginationControl' import { objectDiffShallow, objectsEqual, toParams } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' import { insightsApi } from 'scenes/insights/utils/api' @@ -89,11 +88,6 @@ export const savedInsightsLogic = kea([ addInsight: (insight: QueryBasedInsightModel) => ({ insight }), openAlertModal: (alertId: AlertType['id']) => ({ alertId }), closeAlertModal: true, - addInsightToDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ insight, dashboardId }), - removeInsightFromDashboard: (insight: QueryBasedInsightModel, dashboardId: number) => ({ - insight, - dashboardId, - }), setDashboardUpdateLoading: (insightId: number, loading: boolean) => ({ insightId, loading }), }), loaders(({ values }) => ({ @@ -309,45 +303,6 @@ export const savedInsightsLogic = kea([ setDates: () => { actions.loadInsights() }, - addInsightToDashboard: async ({ insight, dashboardId }) => { - try { - actions.setDashboardUpdateLoading(insight.id, true) - - const response = await insightsApi.update(insight.id, { - dashboards: [...(insight.dashboards || []), dashboardId], - }) - if (response) { - actions.updateInsight(response) - const logic = dashboardLogic({ id: dashboardId }) - logic.mount() - logic.actions.loadDashboard({ action: 'update' }) - logic.unmount() - lemonToast.success('Insight added to dashboard') - } - } finally { - actions.setDashboardUpdateLoading(insight.id, false) - } - }, - removeInsightFromDashboard: async ({ insight, dashboardId }) => { - try { - actions.setDashboardUpdateLoading(insight.id, true) - - const response = await insightsApi.update(insight.id, { - dashboards: (insight.dashboards || []).filter((d) => d !== dashboardId), - dashboard_tiles: (insight.dashboard_tiles || []).filter((dt) => dt.dashboard_id !== dashboardId), - }) - if (response) { - actions.updateInsight(response) - const logic = dashboardLogic({ id: dashboardId }) - logic.mount() - logic.actions.loadDashboard({ action: 'update' }) - logic.unmount() - lemonToast.success('Insight removed from dashboard') - } - } finally { - actions.setDashboardUpdateLoading(insight.id, false) - } - }, [insightsModel.actionTypes.renameInsightSuccess]: ({ item }) => { actions.updateInsight(item) }, From 183944f943c5d540223b9a020bc876f5a84145ad Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jan 2025 16:18:55 -0800 Subject: [PATCH 09/11] fix: remove code breaking test - no longer need scene check --- frontend/src/scenes/saved-insights/savedInsightsLogic.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index d91589bd50135..4fe87a78956bf 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -361,9 +361,6 @@ export const savedInsightsLogic = kea([ { alert_id, ...searchParams }, // search params, hashParams ) => { - if (sceneLogic.findMounted()?.values.activeScene !== Scene.SavedInsights) { - return - } if (alert_id) { actions.openAlertModal(alert_id) } else { From a85d1dae2d40d4e697d9723371d6b4aa72919452 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 8 Jan 2025 06:10:10 -0800 Subject: [PATCH 10/11] fix: add basic flag to load insights --- frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts index 0235c5e022300..e34769d27329a 100644 --- a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts +++ b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts @@ -52,6 +52,7 @@ export const addSavedInsightsModalLogic = kea([ limit: INSIGHTS_PER_PAGE, offset: Math.max(0, (page - 1) * INSIGHTS_PER_PAGE), saved: true, + basic: true, } if (search) { From 9b3b9db38aa583f28bf49180556c4bbb258c9216 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 8 Jan 2025 08:20:47 -0800 Subject: [PATCH 11/11] feat: add event logging --- .../src/scenes/saved-insights/addSavedInsightsModalLogic.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts index e34769d27329a..485e79c609057 100644 --- a/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts +++ b/frontend/src/scenes/saved-insights/addSavedInsightsModalLogic.ts @@ -161,6 +161,7 @@ export const addSavedInsightsModalLogic = kea([ lemonToast.success('Insight added to dashboard') } } finally { + eventUsageLogic.actions.reportSavedInsightToDashboard() actions.setDashboardUpdateLoading(insight.id, false) } }, @@ -180,6 +181,7 @@ export const addSavedInsightsModalLogic = kea([ lemonToast.success('Insight removed from dashboard') } } finally { + eventUsageLogic.actions.reportRemovedInsightFromDashboard() actions.setDashboardUpdateLoading(insight.id, false) } },