From e6217f6484cadd29569b7438fcc2202f77754e72 Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Wed, 28 Oct 2020 16:28:22 +0000 Subject: [PATCH] Dashboard empty states (#2068) --- frontend/public/empty-line-graph-dark.svg | 22 +++ frontend/public/empty-line-graph.svg | 22 +++ .../src/scenes/dashboard/DashboardItem.js | 13 +- .../src/scenes/dashboard/DashboardItems.scss | 17 ++ frontend/src/scenes/dashboard/Dashboards.tsx | 2 +- .../src/scenes/dashboard/NewDashboard.tsx | 2 +- .../src/scenes/insights/ActionsLineGraph.js | 5 +- frontend/src/scenes/insights/EmptyStates.tsx | 38 ++++ latest_migrations.manifest | 2 +- posthog/api/dashboard.py | 36 +++- posthog/api/test/test_organization.py | 184 +++++++++++++++++- posthog/helpers/dashboard_templates.py | 15 +- .../0094_description_on_dashboard_items.py | 19 ++ posthog/models/dashboard.py | 13 ++ posthog/models/dashboard_item.py | 4 + posthog/models/team.py | 81 ++++---- posthog/models/user.py | 6 +- 17 files changed, 428 insertions(+), 53 deletions(-) create mode 100644 frontend/public/empty-line-graph-dark.svg create mode 100644 frontend/public/empty-line-graph.svg create mode 100644 frontend/src/scenes/insights/EmptyStates.tsx create mode 100644 posthog/migrations/0094_description_on_dashboard_items.py diff --git a/frontend/public/empty-line-graph-dark.svg b/frontend/public/empty-line-graph-dark.svg new file mode 100644 index 0000000000000..ce35172859569 --- /dev/null +++ b/frontend/public/empty-line-graph-dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/empty-line-graph.svg b/frontend/public/empty-line-graph.svg new file mode 100644 index 0000000000000..226b13957f1a5 --- /dev/null +++ b/frontend/public/empty-line-graph.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/scenes/dashboard/DashboardItem.js b/frontend/src/scenes/dashboard/DashboardItem.js index b0499f524458c..2b2cc2accf7c1 100644 --- a/frontend/src/scenes/dashboard/DashboardItem.js +++ b/frontend/src/scenes/dashboard/DashboardItem.js @@ -1,6 +1,7 @@ +import './DashboardItems.scss' import { Link } from 'lib/components/Link' import { useActions, useValues } from 'kea' -import { Dropdown, Menu, Tooltip, Alert } from 'antd' +import { Dropdown, Menu, Tooltip, Alert, Button } from 'antd' import { combineUrl, router } from 'kea-router' import { deleteWithUndo, Loading } from 'lib/utils' import React, { useEffect, useState } from 'react' @@ -133,6 +134,11 @@ export function DashboardItem({ {...longPressProps} data-attr={'dashboard-item-' + index} > + {item.is_sample && ( +
+ +
+ )}
@@ -149,6 +155,7 @@ export function DashboardItem({ router.actions.push(link) } }} + style={{ fontSize: 16, fontWeight: '500' }} > {item.name} @@ -307,6 +314,10 @@ export function DashboardItem({
)}
+ {item.description && ( +
{item.description}
+ )} +
{Element ? ( diff --git a/frontend/src/scenes/dashboard/DashboardItems.scss b/frontend/src/scenes/dashboard/DashboardItems.scss index a0644edd7b5ea..f56fe3b631ebf 100644 --- a/frontend/src/scenes/dashboard/DashboardItems.scss +++ b/frontend/src/scenes/dashboard/DashboardItems.scss @@ -153,6 +153,23 @@ } } } + + .sample-dasbhoard-overlay { + background-color: rgba(0, 0, 0, 0.7); + position: absolute; + padding: 32px; + right: 0; + bottom: 0; + left: 0; + display: none; + align-items: center; + justify-content: center; + z-index: 100; + } + + &:hover .sample-dasbhoard-overlay { + display: flex; + } } .react-grid-layout { diff --git a/frontend/src/scenes/dashboard/Dashboards.tsx b/frontend/src/scenes/dashboard/Dashboards.tsx index dfcd9e9928dc3..ad7852bccdd5c 100644 --- a/frontend/src/scenes/dashboard/Dashboards.tsx +++ b/frontend/src/scenes/dashboard/Dashboards.tsx @@ -120,7 +120,7 @@ function _Dashboards(): JSX.Element { style={{ cursor: 'pointer' }} onClick={() => addDashboard({ - name: 'Default App Dashboard', + name: 'Web App Dashboard', show: true, useTemplate: 'DEFAULT_APP', }) diff --git a/frontend/src/scenes/dashboard/NewDashboard.tsx b/frontend/src/scenes/dashboard/NewDashboard.tsx index 5afab84ba45d3..b7d62c3d66316 100644 --- a/frontend/src/scenes/dashboard/NewDashboard.tsx +++ b/frontend/src/scenes/dashboard/NewDashboard.tsx @@ -37,7 +37,7 @@ export function NewDashboard(): JSX.Element { Empty Dashboard - Default Dashboard - App + Default Dashboard - Web App diff --git a/frontend/src/scenes/insights/ActionsLineGraph.js b/frontend/src/scenes/insights/ActionsLineGraph.js index 15d9e4932d8cb..5cb577bdd778d 100644 --- a/frontend/src/scenes/insights/ActionsLineGraph.js +++ b/frontend/src/scenes/insights/ActionsLineGraph.js @@ -4,6 +4,7 @@ import { LineGraph } from './LineGraph' import { useActions, useValues } from 'kea' import { trendsLogic } from 'scenes/insights/trendsLogic' import { router } from 'kea-router' +import { LineGraphEmptyState } from './EmptyStates' export function ActionsLineGraph({ dashboardItemId = null, @@ -46,9 +47,7 @@ export function ActionsLineGraph({ } /> ) : ( -

- We couldn't find any matching events. Try changing dates or pick another action or event. -

+ ) ) : ( diff --git a/frontend/src/scenes/insights/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates.tsx new file mode 100644 index 0000000000000..04e26e04300de --- /dev/null +++ b/frontend/src/scenes/insights/EmptyStates.tsx @@ -0,0 +1,38 @@ +import { useValues } from 'kea' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import React from 'react' +import imgEmptyLineGraph from 'public/empty-line-graph.svg' +import imgEmptyLineGraphDark from 'public/empty-line-graph-dark.svg' +import { QuestionCircleOutlined } from '@ant-design/icons' + +export function LineGraphEmptyState({ color }: { color: string }): JSX.Element { + const { featureFlags } = useValues(featureFlagLogic) + return ( + <> + {!featureFlags['1694-dashboards'] && ( +

+ We couldn't find any matching events. Try changing dates or pick another action or event. +

+ )} + {featureFlags['1694-dashboards'] && ( +
+ +
+ Seems like there's no data to show this graph yet{' '} + + + +
+
+ )} + + ) +} diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 8b295e51565f3..a7bff429a934b 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -2,7 +2,7 @@ admin: 0003_logentry_add_action_flag_choices auth: 0011_update_proxy_permissions contenttypes: 0002_remove_content_type_name ee: 0002_hook -posthog: 0093_remove_user_is_superuser +posthog: 0094_description_on_dashboard_items rest_hooks: 0002_swappable_hook_model sessions: 0001_initial social_django: 0008_partial_timestamp diff --git a/posthog/api/dashboard.py b/posthog/api/dashboard.py index bd1f31a470701..50dfe9e950ac7 100644 --- a/posthog/api/dashboard.py +++ b/posthog/api/dashboard.py @@ -2,15 +2,17 @@ from distutils.util import strtobool from typing import Any, Dict +import posthoganalytics from django.core.cache import cache -from django.db.models import Prefetch, QuerySet +from django.db.models import Model, Prefetch, QuerySet from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.utils.timezone import now from django.views.decorators.clickjacking import xframe_options_exempt -from rest_framework import authentication, request, response, serializers, viewsets +from rest_framework import authentication, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import AuthenticationFailed +from rest_framework.request import Request from posthog.auth import PersonalAPIKeyAuthentication, PublicTokenAuthentication from posthog.helpers import create_dashboard_from_template @@ -58,6 +60,12 @@ def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Dashboard: team=team, ) + posthoganalytics.capture( + request.user.distinct_id, + "dashboard created", + {**dashboard.get_analytics_metadata(), "from_template": bool(use_template), "template_key": use_template}, + ) + return dashboard def update( # type: ignore @@ -66,7 +74,15 @@ def update( # type: ignore validated_data.pop("use_template", None) # Remove attribute if present if validated_data.get("is_shared") and not instance.share_token: instance.share_token = secrets.token_urlsafe(22) - return super().update(instance, validated_data) + + instance = super().update(instance, validated_data) + + if "request" in self.context: + posthoganalytics.capture( + self.context["request"].user.distinct_id, "dashboard updated", instance.get_analytics_metadata() + ) + + return instance def get_items(self, dashboard: Dashboard): if self.context["view"].action == "list": @@ -103,7 +119,7 @@ def get_queryset(self) -> QuerySet: return queryset.filter(team=self.request.user.team) - def retrieve(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: + def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> response.Response: pk = kwargs["pk"] queryset = self.get_queryset() dashboard = get_object_or_404(queryset, pk=pk) @@ -121,6 +137,7 @@ class Meta: fields = [ "id", "name", + "description", "filters", "order", "type", @@ -131,8 +148,9 @@ class Meta: "last_refresh", "refreshing", "result", - "created_at", + "is_sample", "saved", + "created_at", "created_by", ] @@ -155,6 +173,12 @@ def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> DashboardIt else: raise serializers.ValidationError("Dashboard not found") + def update(self, instance: Model, validated_data: Dict) -> DashboardItem: + + # Remove is_sample if it's set as user has altered the sample configuration + validated_data.setdefault("is_sample", False) + return super().update(instance, validated_data) + def get_result(self, dashboard_item: DashboardItem): if not dashboard_item.filters: return None @@ -184,7 +208,7 @@ def get_queryset(self) -> QuerySet: return queryset.filter(team=self.request.user.team) - def _filter_request(self, request: request.Request, queryset: QuerySet) -> QuerySet: + def _filter_request(self, request: Request, queryset: QuerySet) -> QuerySet: filters = request.GET.dict() for key in filters: diff --git a/posthog/api/test/test_organization.py b/posthog/api/test/test_organization.py index 88772a0b4eb5e..44f33f49024e3 100644 --- a/posthog/api/test/test_organization.py +++ b/posthog/api/test/test_organization.py @@ -1,4 +1,9 @@ -from posthog.models.organization import Organization, OrganizationMembership +from unittest.mock import patch + +from django.test import tag +from rest_framework import status + +from posthog.models import Dashboard, Organization, OrganizationMembership, Team, User from .base import APIBaseTest @@ -30,3 +35,180 @@ def test_rename_organization_without_license_if_admin(self): self.organization_membership.save() response = self.client.patch(f"/api/organizations/{self.organization.id}", {"name": "ASDFG"}) self.assertEqual(response.status_code, 403) + + +class TestSignup(APIBaseTest): + CONFIG_USER_EMAIL = None + + @tag("skip_on_multitenancy") + @patch("posthog.api.team.settings.EE_AVAILABLE", False) + @patch("posthog.api.team.posthoganalytics.identify") + @patch("posthog.api.team.posthoganalytics.capture") + def test_api_sign_up(self, mock_capture, mock_identify): + response = self.client.post( + "/api/signup/", + { + "first_name": "John", + "email": "hedgehog@posthog.com", + "password": "notsecure", + "company_name": "Hedgehogs United, LLC", + "email_opt_in": False, + }, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + user: User = User.objects.order_by("-pk")[0] + team: Team = user.team + organization: Organization = user.organization + self.assertEqual( + response.data, + {"id": user.pk, "distinct_id": user.distinct_id, "first_name": "John", "email": "hedgehog@posthog.com"}, + ) + + # Assert that the user was properly created + self.assertEqual(user.first_name, "John") + self.assertEqual(user.email, "hedgehog@posthog.com") + self.assertEqual(user.email_opt_in, False) + + # Assert that the team was properly created + self.assertEqual(team.name, "Default Project") + + # Assert that the org was properly created + self.assertEqual(organization.name, "Hedgehogs United, LLC") + + # Assert that the sign up event & identify calls were sent to PostHog analytics + mock_capture.assert_called_once_with( + user.distinct_id, "user signed up", properties={"is_first_user": True, "is_organization_first_user": True}, + ) + + mock_identify.assert_called_once_with( + user.distinct_id, properties={"email": "hedgehog@posthog.com", "realm": "hosted", "ee_available": False}, + ) + + # Assert that the user is logged in + response = self.client.get("/api/user/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["email"], "hedgehog@posthog.com") + + # Assert that the password was correctly saved + self.assertTrue(user.check_password("notsecure")) + + @tag("skip_on_multitenancy") + @patch("posthog.api.team.posthoganalytics.capture") + def test_sign_up_minimum_attrs(self, mock_capture): + response = self.client.post( + "/api/signup/", {"first_name": "Jane", "email": "hedgehog2@posthog.com", "password": "notsecure"}, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + user: User = User.objects.order_by("-pk").get() + organization: Organization = user.organization + self.assertEqual( + response.data, + {"id": user.pk, "distinct_id": user.distinct_id, "first_name": "Jane", "email": "hedgehog2@posthog.com",}, + ) + + # Assert that the user was properly created + self.assertEqual(user.first_name, "Jane") + self.assertEqual(user.email, "hedgehog2@posthog.com") + self.assertEqual(user.email_opt_in, True) # Defaults to True + self.assertEqual(organization.name, "Jane") + + # Assert that the sign up event & identify calls were sent to PostHog analytics + mock_capture.assert_called_once_with( + user.distinct_id, "user signed up", properties={"is_first_user": True, "is_organization_first_user": True}, + ) + + # Assert that the user is logged in + response = self.client.get("/api/user/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["email"], "hedgehog2@posthog.com") + + # Assert that the password was correctly saved + self.assertTrue(user.check_password("notsecure")) + + def test_cant_sign_up_without_required_attributes(self): + count: int = User.objects.count() + team_count: int = Team.objects.count() + + required_attributes = [ + "first_name", + "email", + "password", + ] + + for attribute in required_attributes: + body = { + "first_name": "Jane", + "email": "invalid@posthog.com", + "password": "notsecure", + } + body.pop(attribute) + + # Make sure the endpoint works with and without the trailing slash + response = self.client.post("/api/signup", body) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data, + { + "type": "validation_error", + "code": "required", + "detail": "This field is required.", + "attr": attribute, + }, + ) + + self.assertEqual(User.objects.count(), count) + self.assertEqual(Team.objects.count(), team_count) + + def test_cant_sign_up_with_short_password(self): + count: int = User.objects.count() + team_count: int = Team.objects.count() + + response = self.client.post( + "/api/signup/", {"first_name": "Jane", "email": "failed@posthog.com", "password": "123"}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data, + { + "type": "validation_error", + "code": "password_too_short", + "detail": "This password is too short. It must contain at least 8 characters.", + "attr": "password", + }, + ) + + self.assertEqual(User.objects.count(), count) + self.assertEqual(Team.objects.count(), team_count) + + @patch("posthog.models.team.posthoganalytics.feature_enabled") + def test_default_dashboard_is_created_on_signup(self, mock_feature_enabled): + """ + Tests that the default web app dashboard is created on signup. + Note: This feature is currently behind a feature flag. + """ + + mock_feature_enabled.return_value = True + + response = self.client.post( + "/api/signup/", {"first_name": "Jane", "email": "hedgehog75@posthog.com", "password": "notsecure"}, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + user: User = User.objects.order_by("-pk").get() + + mock_feature_enabled.assert_called_with("1694-dashboards", user.distinct_id) + + self.assertEqual( + response.data, + {"id": user.pk, "distinct_id": user.distinct_id, "first_name": "Jane", "email": "hedgehog75@posthog.com"}, + ) + + dashboard: Dashboard = Dashboard.objects.last() # type: ignore + self.assertEqual(dashboard.team, user.team) + self.assertEqual(dashboard.items.count(), 7) + self.assertEqual(dashboard.name, "My App Dashboard") + self.assertEqual( + dashboard.items.all()[0].description, "Shows the number of unique users that use your app everyday." + ) diff --git a/posthog/helpers/dashboard_templates.py b/posthog/helpers/dashboard_templates.py index 88de2afe0b762..4b961def821fe 100644 --- a/posthog/helpers/dashboard_templates.py +++ b/posthog/helpers/dashboard_templates.py @@ -19,7 +19,8 @@ TRENDS_PIE, TRENDS_STICKINESS, ) -from posthog.models import Dashboard, DashboardItem +from posthog.models.dashboard import Dashboard +from posthog.models.dashboard_item import DashboardItem DASHBOARD_COLORS: List[str] = ["white", "blue", "green", "purple", "black"] @@ -36,6 +37,7 @@ def _create_default_app_items(dashboard: Dashboard) -> None: INTERVAL: "day", }, last_refresh=now(), + description="Shows the number of unique users that use your app everyday.", ) DashboardItem.objects.create( @@ -52,6 +54,8 @@ def _create_default_app_items(dashboard: Dashboard) -> None: }, last_refresh=now(), color=random.choice(DASHBOARD_COLORS), + description="Shows how much revenue your app is capturing from orders every week. " + 'Sales should be registered with an "Order Completed" event.', ) DashboardItem.objects.create( @@ -67,6 +71,7 @@ def _create_default_app_items(dashboard: Dashboard) -> None: }, last_refresh=now(), color=random.choice(DASHBOARD_COLORS), + description="Shows the total cumulative number of unique users that have been using your app.", ) DashboardItem.objects.create( @@ -77,11 +82,12 @@ def _create_default_app_items(dashboard: Dashboard) -> None: filters={ TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "math": "dau", "type": TREND_FILTER_TYPE_EVENTS}], INTERVAL: "day", - DATE_FROM: "-30d", SHOWN_AS: TRENDS_STICKINESS, }, last_refresh=now(), color=random.choice(DASHBOARD_COLORS), + description="Shows you how many users visited your app for a specific number of days " + '(e.g. a user that visited your app twice in the time period will be shown under "2 days").', ) DashboardItem.objects.create( @@ -118,6 +124,9 @@ def _create_default_app_items(dashboard: Dashboard) -> None: }, last_refresh=now(), color=random.choice(DASHBOARD_COLORS), + is_sample=True, + description="This is a sample of how a user funnel could look like. It represents the number of users that performed " + "a specific action at each step.", ) DashboardItem.objects.create( @@ -134,6 +143,7 @@ def _create_default_app_items(dashboard: Dashboard) -> None: DISPLAY: TRENDS_PIE, }, last_refresh=now(), + description="Shows a breakdown of browsers used to visit your app per unique users in the last 14 days.", ) DashboardItem.objects.create( @@ -148,6 +158,7 @@ def _create_default_app_items(dashboard: Dashboard) -> None: BREAKDOWN: "$initial_referring_domain", }, last_refresh=now(), + description="Shows a breakdown of where unique users came from when visiting your app.", ) diff --git a/posthog/migrations/0094_description_on_dashboard_items.py b/posthog/migrations/0094_description_on_dashboard_items.py new file mode 100644 index 0000000000000..26cd2ec275bea --- /dev/null +++ b/posthog/migrations/0094_description_on_dashboard_items.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-10-28 12:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("posthog", "0093_remove_user_is_superuser"), + ] + + operations = [ + migrations.AddField( + model_name="dashboarditem", + name="description", + field=models.CharField(blank=True, max_length=400, null=True), + ), + migrations.AddField(model_name="dashboarditem", name="is_sample", field=models.BooleanField(default=False),), + ] diff --git a/posthog/models/dashboard.py b/posthog/models/dashboard.py index ca2f18d10a7ad..0d5ccb1bbcf6a 100644 --- a/posthog/models/dashboard.py +++ b/posthog/models/dashboard.py @@ -1,3 +1,5 @@ +from typing import Any, Dict + from django.db import models @@ -11,3 +13,14 @@ class Dashboard(models.Model): share_token: models.CharField = models.CharField(max_length=400, null=True, blank=True) is_shared: models.BooleanField = models.BooleanField(default=False) last_accessed_at: models.DateTimeField = models.DateTimeField(blank=True, null=True) + + def get_analytics_metadata(self) -> Dict[str, Any]: + """ + Returns serialized information about the object for analytics reporting. + """ + return { + "pinned": self.pinned, + "item_count": self.items.count(), + "is_shared": self.is_shared, + "created_at": self.created_at, + } diff --git a/posthog/models/dashboard_item.py b/posthog/models/dashboard_item.py index 092bb095c4aff..5bc013679917d 100644 --- a/posthog/models/dashboard_item.py +++ b/posthog/models/dashboard_item.py @@ -7,6 +7,7 @@ class DashboardItem(models.Model): "Dashboard", related_name="items", on_delete=models.CASCADE, null=True, blank=True ) name: models.CharField = models.CharField(max_length=400, null=True, blank=True) + description: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) filters: JSONField = JSONField(default=dict) order: models.IntegerField = models.IntegerField(null=True, blank=True) @@ -20,3 +21,6 @@ class DashboardItem(models.Model): refreshing: models.BooleanField = models.BooleanField(default=False) funnel: models.ForeignKey = models.ForeignKey("Funnel", on_delete=models.CASCADE, null=True, blank=True) created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True, blank=True) + is_sample: models.BooleanField = models.BooleanField( + default=False + ) # indicates if it's a sample graph generated by dashboard templates diff --git a/posthog/models/team.py b/posthog/models/team.py index 62213719e82ea..d333d906e1b6b 100644 --- a/posthog/models/team.py +++ b/posthog/models/team.py @@ -6,6 +6,7 @@ from django.utils import timezone from posthog.constants import TREND_FILTER_TYPE_EVENTS, TRENDS_LINEAR +from posthog.helpers.dashboard_templates import create_dashboard_from_template from .action import Action from .action_step import ActionStep @@ -18,46 +19,56 @@ class TeamManager(models.Manager): - def create_with_data(self, users: Optional[List[Any]] = None, **kwargs) -> "Team": + def create_with_data(self, user=None, **kwargs) -> "Team": team = Team.objects.create(**kwargs) - if not users or not posthoganalytics.feature_enabled("actions-ux-201012", users[0].distinct_id): - # Don't create default `Pageviews` action on actions-ux-201012 feature flag (use first user as proxy for org) + if not user or not posthoganalytics.feature_enabled("actions-ux-201012", user.distinct_id): + # Don't create default `Pageviews` action on actions-ux-201012 feature flag action = Action.objects.create(team=team, name="Pageviews") ActionStep.objects.create(action=action, event="$pageview") - dashboard = Dashboard.objects.create( - name="Default", pinned=True, team=team, share_token=generate_random_token() - ) - - DashboardItem.objects.create( - team=team, - dashboard=dashboard, - name="Pageviews this week", - type=TRENDS_LINEAR, - filters={TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "type": TREND_FILTER_TYPE_EVENTS}]}, - last_refresh=timezone.now(), - ) - DashboardItem.objects.create( - team=team, - dashboard=dashboard, - name="Most popular browsers this week", - type="ActionsTable", - filters={ - TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "type": TREND_FILTER_TYPE_EVENTS}], - "display": "ActionsTable", - "breakdown": "$browser", - }, - last_refresh=timezone.now(), - ) - DashboardItem.objects.create( - team=team, - dashboard=dashboard, - name="Daily Active Users", - type=TRENDS_LINEAR, - filters={TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "math": "dau", "type": TREND_FILTER_TYPE_EVENTS}]}, - last_refresh=timezone.now(), - ) + # Create default dashboard + if user and posthoganalytics.feature_enabled("1694-dashboards", user.distinct_id): + # Create app template dashboard if feature flag is active + dashboard = Dashboard.objects.create(name="My App Dashboard", pinned=True, team=team,) + create_dashboard_from_template("DEFAULT_APP", dashboard) + else: + # DEPRECATED: Will be retired in favor of dashboard_templates.py + dashboard = Dashboard.objects.create( + name="Default", pinned=True, team=team, share_token=generate_random_token() + ) + + DashboardItem.objects.create( + team=team, + dashboard=dashboard, + name="Pageviews this week", + type=TRENDS_LINEAR, + filters={TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "type": TREND_FILTER_TYPE_EVENTS}]}, + last_refresh=timezone.now(), + ) + DashboardItem.objects.create( + team=team, + dashboard=dashboard, + name="Most popular browsers this week", + type="ActionsTable", + filters={ + TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "type": TREND_FILTER_TYPE_EVENTS}], + "display": "ActionsTable", + "breakdown": "$browser", + }, + last_refresh=timezone.now(), + ) + DashboardItem.objects.create( + team=team, + dashboard=dashboard, + name="Daily Active Users", + type=TRENDS_LINEAR, + filters={ + TREND_FILTER_TYPE_EVENTS: [{"id": "$pageview", "math": "dau", "type": TREND_FILTER_TYPE_EVENTS}] + }, + last_refresh=timezone.now(), + ) + return team def get_team_from_token(self, token: str, is_personal_api_key: bool = False) -> Optional["Team"]: diff --git a/posthog/models/user.py b/posthog/models/user.py index 431ece4cc573b..102aa0c955a1a 100644 --- a/posthog/models/user.py +++ b/posthog/models/user.py @@ -61,9 +61,11 @@ def bootstrap( organization_fields = organization_fields or {} organization_fields.setdefault("name", company_name) organization = Organization.objects.create(**organization_fields) - team = Team.objects.create_with_data(organization=organization, **(team_fields or {})) user = self.create_user(email=email, password=password, first_name=first_name, **user_fields) - membership = user.join(organization=organization, team=team, level=OrganizationMembership.Level.ADMIN,) + team = Team.objects.create_with_data(user=user, organization=organization, **(team_fields or {})) + user.join( + organization=organization, team=team, level=OrganizationMembership.Level.ADMIN, + ) return organization, team, user def create_and_join(