0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 21:49:51 +01:00

feat: Person feed map (#18184)

This commit is contained in:
Ben White 2023-10-26 09:20:43 +02:00 committed by GitHub
parent 8a188cc192
commit 4412c2cbeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 719 additions and 198 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
MAPLIBRE_STYLE_URL=https://api.example.com/style.json?key=mykey

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -6,6 +6,7 @@ declare global {
JS_POSTHOG_API_KEY?: string
JS_POSTHOG_HOST?: string
JS_POSTHOG_SELF_CAPTURE?: boolean
JS_MAPLIBRE_STYLE_URL?: string
JS_CAPTURE_TIME_TO_SEE_DATA?: boolean
JS_KEA_VERBOSE_LOGGING?: boolean
posthog?: posthog

View File

@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Marker } from 'maplibre-gl'
import { Map, MapComponent } from './Map'
const meta: Meta<typeof Map> = {
title: 'Components/Map',
component: Map,
tags: ['autodocs'],
}
type Story = StoryObj<typeof Map>
const coordinates: [number, number] = [0.119167, 52.205276]
export const Unavailable: Story = {}
export const Basic: Story = {
render: (args) => (
<MapComponent
mapLibreStyleUrl="" // TODO: set this value for the publish storybook and visual regression tests
{...args}
/>
),
args: {
center: coordinates,
markers: [new Marker({ color: 'var(--primary)' }).setLngLat(coordinates)],
className: 'h-60',
},
}
export default meta

View File

@ -0,0 +1,64 @@
import { useEffect, useRef } from 'react'
import { Map as RawMap, Marker } from 'maplibre-gl'
import useResizeObserver from 'use-resize-observer'
import 'maplibre-gl/dist/maplibre-gl.css'
/** Latitude and longtitude in degrees (+lat is east, -lat is west, +lon is south, -lon is north). */
export interface MapProps {
/** Coordinates to center the map on by default. */
center: [number, number]
/** Markers to show. */
markers?: Marker[]
/** Map container class names. */
className?: string
/** The map's MapLibre style. This must be a JSON object conforming to the schema described in the MapLibre Style Specification, or a URL to such JSON. */
mapLibreStyleUrl: string
}
export function Map({ className, ...rest }: Omit<MapProps, 'mapLibreStyleUrl'>): JSX.Element {
if (!window.JS_MAPLIBRE_STYLE_URL) {
return (
<div className={`w-full h-full flex flex-col items-center justify-center text-muted p-3 ${className}`}>
<h1>Map unavailable</h1>
<p>
The <code>MAPLIBRE_STYLE_URL</code> setting is not defined. Please configure this setting with a
valid MapLibre Style URL to display maps.
</p>
</div>
)
}
return <MapComponent mapLibreStyleUrl={window.JS_MAPLIBRE_STYLE_URL} className={className} {...rest} />
}
export function MapComponent({ center, markers, className, mapLibreStyleUrl }: MapProps): JSX.Element {
const mapContainer = useRef<HTMLDivElement>(null)
const map = useRef<RawMap | null>(null)
useEffect(() => {
map.current = new RawMap({
container: mapContainer.current as HTMLElement,
style: mapLibreStyleUrl,
center,
zoom: 4,
maxZoom: 10,
})
if (markers) {
for (const marker of markers) {
marker.addTo(map.current)
}
}
}, [])
useResizeObserver({
ref: mapContainer,
onResize: () => {
if (map.current) {
map.current.resize()
}
},
})
return <div ref={mapContainer} className={className} />
}

View File

@ -0,0 +1,64 @@
import { Marker } from 'maplibre-gl'
import { NotebookNodeType } from '~/types'
import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper'
import { personLogic } from 'scenes/persons/personLogic'
import { useValues } from 'kea'
import { LemonSkeleton } from '@posthog/lemon-ui'
import { NotFound } from 'lib/components/NotFound'
import { Map } from '../../../lib/components/Map/Map'
import { notebookNodeLogic } from './notebookNodeLogic'
import { NotebookNodeProps } from 'scenes/notebooks/Notebook/utils'
import { NotebookNodeEmptyState } from './components/NotebookNodeEmptyState'
const Component = ({ attributes }: NotebookNodeProps<NotebookNodeMapAttributes>): JSX.Element | null => {
const { id } = attributes
const { expanded } = useValues(notebookNodeLogic)
const logic = personLogic({ id })
const { person, personLoading } = useValues(logic)
if (personLoading) {
return <LemonSkeleton className="h-6" />
} else if (!person) {
return <NotFound object="person" />
}
if (!expanded) {
return null
}
const longtitude = person?.properties?.['$geoip_longitude']
const latitude = person?.properties?.['$geoip_latitude']
const personCoordinates: [number, number] | null =
!isNaN(longtitude) && !isNaN(latitude) ? [longtitude, latitude] : null
if (!personCoordinates) {
return <NotebookNodeEmptyState message="No map available." />
}
return (
<Map
center={personCoordinates}
markers={[new Marker({ color: 'var(--primary)' }).setLngLat(personCoordinates)]}
className="h-full"
/>
)
}
type NotebookNodeMapAttributes = {
id: string
}
export const NotebookNodeMap = createPostHogWidgetNode<NotebookNodeMapAttributes>({
nodeType: NotebookNodeType.Map,
titlePlaceholder: 'Location',
Component,
resizeable: true,
heightEstimate: 150,
expandable: true,
startExpanded: true,
attributes: {
id: {},
},
})

View File

@ -0,0 +1,11 @@
type NotebookNodeEmptyStateProps = {
message: string
}
export function NotebookNodeEmptyState({ message }: NotebookNodeEmptyStateProps): JSX.Element {
return (
<div className="w-full h-full flex flex-col items-center justify-center text-muted p-3">
<i>{message}</i>
</div>
)
}

View File

@ -39,6 +39,7 @@ import { sampleOne } from 'lib/utils'
import { NotebookNodeGroup } from '../Nodes/NotebookNodeGroup'
import { NotebookNodeCohort } from '../Nodes/NotebookNodeCohort'
import { NotebookNodePersonFeed } from '../Nodes/NotebookNodePersonFeed/NotebookNodePersonFeed'
import { NotebookNodeMap } from '../Nodes/NotebookNodeMap'
const CustomDocument = ExtensionDocument.extend({
content: 'heading block*',
@ -120,6 +121,7 @@ export function Editor(): JSX.Element {
BacklinkCommandsExtension,
NodeGapInsertionExtension,
NotebookNodePersonFeed,
NotebookNodeMap,
],
editorProps: {
handleDrop: (view, event, _slice, moved) => {

View File

@ -125,6 +125,7 @@ export const textContent = (node: any): string => {
'ph-group': customOrTitleSerializer,
'ph-cohort': customOrTitleSerializer,
'ph-person-feed': customOrTitleSerializer,
'ph-map': customOrTitleSerializer,
}
return getText(node, {

View File

@ -4,7 +4,7 @@ import { NotebooksListFilters } from 'scenes/notebooks/NotebooksTable/notebooksT
export const fromNodeTypeToLabel: Omit<
Record<NotebookNodeType, string>,
NotebookNodeType.Backlink | NotebookNodeType.PersonFeed
NotebookNodeType.Backlink | NotebookNodeType.PersonFeed | NotebookNodeType.Map
> = {
[NotebookNodeType.FeatureFlag]: 'Feature flags',
[NotebookNodeType.FeatureFlagCodeExample]: 'Feature flag Code Examples',

View File

@ -32,6 +32,10 @@ const PersonFeedCanvas = ({ person }: PersonFeedCanvasProps): JSX.Element => {
type: 'ph-person',
attrs: { id: personId, nodeId: uuid(), title: 'Info' },
},
{
type: 'ph-map',
attrs: { id: personId, nodeId: uuid() },
},
],
},
},

View File

@ -3082,6 +3082,7 @@ export enum NotebookNodeType {
ReplayTimestamp = 'ph-replay-timestamp',
Image = 'ph-image',
PersonFeed = 'ph-person-feed',
Map = 'ph-map',
}
export type NotebookNodeResource = {

View File

@ -130,6 +130,7 @@
"kea-test-utils": "^0.2.4",
"kea-waitfor": "^0.2.1",
"kea-window-values": "^3.0.0",
"maplibre-gl": "^3.5.1",
"md5": "^2.3.0",
"monaco-editor": "^0.39.0",
"papaparse": "^5.4.1",

File diff suppressed because it is too large Load Diff

View File

@ -79,6 +79,9 @@ CAPTURE_TIME_TO_SEE_DATA = get_from_env("CAPTURE_TIME_TO_SEE_DATA", False, type_
# Whether kea should be act in verbose mode
KEA_VERBOSE_LOGGING = get_from_env("KEA_VERBOSE_LOGGING", False, type_cast=str_to_bool)
# MapLibre Style URL to configure map tile source
MAPLIBRE_STYLE_URL = get_from_env("MAPLIBRE_STYLE_URL", optional=True)
# Only written in specific scripts - do not use outside of them.
PERSON_ON_EVENTS_OVERRIDE = get_from_env("PERSON_ON_EVENTS_OVERRIDE", optional=True, type_cast=str_to_bool)

View File

@ -36,6 +36,11 @@
window.SENTRY_ENVIRONMENT = '{{ sentry_environment | escapejs }}';
</script>
{% endif %}
{% if js_maplibre_style_url %}
<script>
window.JS_MAPLIBRE_STYLE_URL = '{{ js_maplibre_style_url | escapejs }}'
</script>
{% endif %}
<script id='posthog-app-user-preload'>
window.POSTHOG_APP_CONTEXT = JSON.parse("{{ posthog_app_context | escapejs }}");
// Inject the expected location of JS bundle, use to allow the location of

View File

@ -332,6 +332,9 @@ def render_template(
context["js_kea_verbose_logging"] = settings.KEA_VERBOSE_LOGGING
context["js_url"] = get_js_url(request)
if settings.MAPLIBRE_STYLE_URL:
context["js_maplibre_style_url"] = settings.MAPLIBRE_STYLE_URL
posthog_app_context: Dict[str, Any] = {
"persisted_feature_flags": settings.PERSISTED_FEATURE_FLAGS,
"anonymous": not request.user or not request.user.is_authenticated,