mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 13:39:22 +01:00
Turbo mode (#6632)
* fix router redirect * remove dependence on user var * split scenes and sceneTypes out of sceneLogic * rename LoadedScene type to SceneExport * export SceneExport from most scenes * use exported scene objects, warn if not exported * fix type import bugs * remove dashboard * keep all loaded scene logics in memory * fix sorting bugs * support scenes with params, make it work with dashboards * fetch result from dashboard if mounted * fix mutations * add lastTouch * refactor scene parameters to include searchParams and hashParams * add insights scene to scene router * add insight router scene to scene router * fix cohort back/forward bug * this works * bring back delayed loading of scenes * set insight from dashboard also in afterMount * split events, actions, event stats and properties stats into their own scenes * refactor to options object for setInsight * override filters * clean filters for comparison * fix cohort bug * get a better feature flag * make turbo mode faster by making non-turbo-mode slower * less noise in failed tests * fix tests * flyby: add jest tests pycharm launcher * clean up scenes * add test for loading from dashboardLogic * fix bug * split test init code into two * have the same data in the context and in the api * add basic tests for sceneLogic * run the latest and greatest * fix menu highlight * implement screaming snake * only show scenes with logics
This commit is contained in:
parent
df583d528b
commit
f76d0b6521
11
.run/Jest Tests.run.xml
Normal file
11
.run/Jest Tests.run.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Jest Tests" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="" />
|
||||
<jest-package value="$PROJECT_DIR$/node_modules/jest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
@ -13,7 +13,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { Link } from 'lib/components/Link'
|
||||
import { Scene, sceneLogic } from 'scenes/sceneLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { isMobile } from 'lib/utils'
|
||||
import { useEscapeKey } from 'lib/hooks/useEscapeKey'
|
||||
@ -44,12 +44,16 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { Tooltip } from 'lib/components/Tooltip'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
|
||||
// to show the right page in the sidebar
|
||||
const sceneOverride: Partial<Record<Scene, string>> = {
|
||||
action: 'actions',
|
||||
person: 'persons',
|
||||
dashboard: 'dashboards',
|
||||
const sceneOverride: Partial<Record<Scene, Scene>> = {
|
||||
[Scene.Action]: Scene.Events,
|
||||
[Scene.Actions]: Scene.Events,
|
||||
[Scene.EventStats]: Scene.Events,
|
||||
[Scene.EventPropertyStats]: Scene.Events,
|
||||
[Scene.Person]: Scene.Persons,
|
||||
[Scene.Dashboard]: Scene.Dashboards,
|
||||
}
|
||||
|
||||
interface MenuItemProps {
|
||||
|
@ -29,6 +29,12 @@ export const MOCK_ORGANIZATION_ID: OrganizationType['id'] = 'ABCD'
|
||||
|
||||
export const api = apiReal as any as APIMockReturnType
|
||||
|
||||
export const MOCK_DEFAULT_TEAM = {
|
||||
id: MOCK_TEAM_ID,
|
||||
ingested_event: true,
|
||||
completed_snippet_onboarding: true,
|
||||
}
|
||||
|
||||
export const mockAPI = (cb: (url: APIRoute) => any): void => {
|
||||
beforeEach(async () => {
|
||||
const methods = ['get', 'update', 'create', 'delete']
|
||||
@ -52,11 +58,7 @@ export function defaultAPIMocks(
|
||||
team: { ingested_event: true, completed_snippet_onboarding: true },
|
||||
}
|
||||
} else if (pathname === 'api/projects/@current') {
|
||||
return {
|
||||
id: MOCK_TEAM_ID,
|
||||
ingested_event: true,
|
||||
completed_snippet_onboarding: true,
|
||||
}
|
||||
return MOCK_DEFAULT_TEAM
|
||||
} else if (pathname === 'api/organizations/@current') {
|
||||
return {
|
||||
id: MOCK_ORGANIZATION_ID,
|
||||
|
@ -247,6 +247,7 @@ export const FEATURE_FLAGS = {
|
||||
SIGMA_ANALYSIS: 'sigma-analysis', // owner: @neilkakkar
|
||||
NEW_SESSIONS_PLAYER: 'new-sessions-player', // owner: @rcmarron
|
||||
BREAKDOWN_BY_MULTIPLE_PROPERTIES: '938-breakdown-by-multiple-properties', // owner: @pauldambra
|
||||
TURBO_MODE: 'turbo-mode', // owner: @mariusandra
|
||||
}
|
||||
|
||||
export const ENTITY_MATCH_TYPE = 'entities'
|
||||
|
@ -189,7 +189,7 @@ export const dashboardsModel = kea<dashboardsModelType>({
|
||||
nameSortedDashboards: [
|
||||
() => [selectors.rawDashboards],
|
||||
(rawDashboards) => {
|
||||
return Object.values(rawDashboards).sort((a, b) =>
|
||||
return [...Object.values(rawDashboards)].sort((a, b) =>
|
||||
(a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled')
|
||||
)
|
||||
},
|
||||
@ -198,7 +198,7 @@ export const dashboardsModel = kea<dashboardsModelType>({
|
||||
pinSortedDashboards: [
|
||||
() => [selectors.nameSortedDashboards],
|
||||
(nameSortedDashboards) => {
|
||||
return nameSortedDashboards.sort(
|
||||
return [...nameSortedDashboards].sort(
|
||||
(a, b) =>
|
||||
(Number(b.pinned) - Number(a.pinned)) * 10 +
|
||||
(a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled')
|
||||
|
@ -18,6 +18,7 @@ import { models } from '~/models'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { CloudAnnouncement } from '~/layout/navigation/CloudAnnouncement'
|
||||
import { teamLogic } from './teamLogic'
|
||||
import { LoadedScene } from 'scenes/sceneTypes'
|
||||
|
||||
export const appLogic = kea<appLogicType>({
|
||||
actions: {
|
||||
@ -64,11 +65,13 @@ export function App(): JSX.Element | null {
|
||||
const { user } = useValues(userLogic)
|
||||
const { currentTeamId } = useValues(teamLogic)
|
||||
const { sceneConfig } = useValues(sceneLogic)
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
|
||||
if (showApp) {
|
||||
return (
|
||||
<>
|
||||
{user && currentTeamId ? <Models /> : null}
|
||||
{featureFlags[FEATURE_FLAGS.TURBO_MODE] ? <LoadedSceneLogics /> : null}
|
||||
{(!sceneConfig.projectBased || currentTeamId) && <AppScene />}
|
||||
</>
|
||||
)
|
||||
@ -77,6 +80,27 @@ export function App(): JSX.Element | null {
|
||||
return showingDelayedSpinner ? <SceneLoading /> : null
|
||||
}
|
||||
|
||||
function LoadedSceneLogic({ scene }: { scene: LoadedScene }): null {
|
||||
if (!scene.logic) {
|
||||
throw new Error('Loading scene without a logic')
|
||||
}
|
||||
useMountedLogic(scene.logic(scene.paramsToProps?.(scene.sceneParams)))
|
||||
return null
|
||||
}
|
||||
|
||||
function LoadedSceneLogics(): JSX.Element {
|
||||
const { loadedScenes } = useValues(sceneLogic)
|
||||
return (
|
||||
<>
|
||||
{Object.entries(loadedScenes)
|
||||
.filter(([, { logic }]) => !!logic)
|
||||
.map(([key, loadedScene]) => (
|
||||
<LoadedSceneLogic key={key} scene={loadedScene} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Loads every logic in the "src/models" folder */
|
||||
function Models(): null {
|
||||
useMountedLogic(models)
|
||||
|
@ -17,6 +17,7 @@ import { router } from 'kea-router'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { capitalizeFirstLetter } from 'lib/utils'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
interface PreflightItemInterface {
|
||||
name: string
|
||||
@ -29,6 +30,11 @@ interface CheckInterface extends PreflightItemInterface {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: PreflightCheck,
|
||||
logic: preflightLogic,
|
||||
}
|
||||
|
||||
function PreflightItem({ name, status, caption, failedState }: PreflightItemInterface): JSX.Element {
|
||||
/*
|
||||
status === undefined -> Item still loading (no positive or negative response yet)
|
||||
|
@ -18,6 +18,8 @@ import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { getBreakpoint } from 'lib/utils/responsiveUtils'
|
||||
import { ColumnType } from 'antd/lib/table'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { EventsTab, EventsTabs } from 'scenes/events'
|
||||
import api from '../../lib/api'
|
||||
import { getCurrentTeamId } from '../../lib/utils/logics'
|
||||
|
||||
@ -30,6 +32,12 @@ const searchActions = (sources: ActionType[], search: string): ActionType[] => {
|
||||
.map((result) => result.item)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: ActionsTable,
|
||||
logic: actionsModel,
|
||||
paramsToProps: () => ({ params: 'include_count=1' }),
|
||||
}
|
||||
|
||||
export function ActionsTable(): JSX.Element {
|
||||
const { currentTeam, currentTeamId } = useValues(teamLogic)
|
||||
const { actions, actionsLoading } = useValues(actionsModel({ params: 'include_count=1' }))
|
||||
@ -179,7 +187,8 @@ export function ActionsTable(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-attr="manage-events-table" style={{ paddingTop: 32 }}>
|
||||
<EventsTabs tab={EventsTab.Actions} />
|
||||
<PageHeader
|
||||
title="Actions"
|
||||
caption={
|
||||
|
@ -17,10 +17,16 @@ import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs'
|
||||
import generatePicker from 'antd/es/date-picker/generatePicker'
|
||||
import { normalizeColumnTitle, useIsTableScrolling } from 'lib/components/Table/utils'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
const DatePicker = generatePicker<dayjs.Dayjs>(dayjsGenerateConfig)
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Annotations,
|
||||
logic: annotationsTableLogic,
|
||||
}
|
||||
|
||||
export function Annotations(): JSX.Element {
|
||||
const { annotations, annotationsLoading, next, loadingNext } = useValues(annotationsTableLogic)
|
||||
const { updateAnnotation, deleteAnnotation, loadAnnotationsNext, restoreAnnotation } =
|
||||
|
@ -16,6 +16,12 @@ import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import Checkbox from 'antd/lib/checkbox/Checkbox'
|
||||
import smLogo from 'public/icon-white.svg'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: InviteSignup,
|
||||
logic: inviteSignupLogic,
|
||||
}
|
||||
|
||||
const UTM_TAGS = 'utm_medium=in-product&utm_campaign=invite-signup'
|
||||
const PasswordStrength = lazy(() => import('../../lib/components/PasswordStrength'))
|
||||
|
@ -12,6 +12,12 @@ import { ExclamationCircleFilled } from '@ant-design/icons'
|
||||
import clsx from 'clsx'
|
||||
import { ErrorMessage } from 'lib/components/ErrorMessage/ErrorMessage'
|
||||
import { WelcomeLogo } from './WelcomeLogo'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Login,
|
||||
logic: loginLogic,
|
||||
}
|
||||
|
||||
export function Login(): JSX.Element {
|
||||
const [form] = Form.useForm()
|
||||
|
@ -12,6 +12,12 @@ import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { CodeSnippet, Language } from 'scenes/ingestion/frameworks/CodeSnippet'
|
||||
import { passwordResetLogic } from './passwordResetLogic'
|
||||
import { router } from 'kea-router'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: PasswordReset,
|
||||
logic: passwordResetLogic,
|
||||
}
|
||||
|
||||
export function PasswordReset(): JSX.Element {
|
||||
const { preflight, preflightLoading } = useValues(preflightLogic)
|
||||
|
@ -11,6 +11,12 @@ import { ExclamationCircleFilled, StopOutlined } from '@ant-design/icons'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { passwordResetLogic } from './passwordResetLogic'
|
||||
import { router } from 'kea-router'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: PasswordResetComplete,
|
||||
logic: passwordResetLogic,
|
||||
}
|
||||
|
||||
export function PasswordResetComplete(): JSX.Element {
|
||||
const { validatedResetToken, validatedResetTokenLoading } = useValues(passwordResetLogic)
|
||||
|
@ -13,6 +13,12 @@ import { userLogic } from '../userLogic'
|
||||
import { WelcomeLogo } from './WelcomeLogo'
|
||||
import hedgehogMain from 'public/hedgehog-bridge-page.png'
|
||||
import { ErrorMessage } from 'lib/components/ErrorMessage/ErrorMessage'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Signup,
|
||||
logic: signupLogic,
|
||||
}
|
||||
|
||||
const UTM_TAGS = 'utm_campaign=in-product&utm_tag=signup-header'
|
||||
|
||||
|
@ -6,6 +6,12 @@ import { BillingEnrollment } from './BillingEnrollment'
|
||||
import { useValues } from 'kea'
|
||||
import './Billing.scss'
|
||||
import { billingLogic } from './billingLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Billing,
|
||||
logic: billingLogic,
|
||||
}
|
||||
|
||||
export function Billing(): JSX.Element {
|
||||
const { billing } = useValues(billingLogic)
|
||||
|
@ -2,9 +2,10 @@ import { kea } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { billingLogicType } from './billingLogicType'
|
||||
import { PlanInterface, BillingType } from '~/types'
|
||||
import { sceneLogic, Scene } from 'scenes/sceneLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import posthog from 'posthog-js'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
|
||||
export const UTM_TAGS = 'utm_medium=in-product&utm_campaign=billing-management'
|
||||
export const ALLOCATION_THRESHOLD_ALERT = 0.85 // Threshold to show warning of event usage near limit
|
||||
|
@ -19,6 +19,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { cohortsUrlLogicType } from './CohortsType'
|
||||
import { Link } from 'lib/components/Link'
|
||||
import { PROPERTY_MATCH_TYPE } from 'lib/constants'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
@ -60,6 +61,8 @@ const cohortsUrlLogic = kea<cohortsUrlLogicType>({
|
||||
actions.setOpenCohort(cohort)
|
||||
} else if (cohortId === 'new') {
|
||||
actions.setOpenCohort(NEW_COHORT)
|
||||
} else if (!cohortId) {
|
||||
actions.setOpenCohort(null)
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -221,3 +224,8 @@ export function Cohorts(): JSX.Element {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Cohorts,
|
||||
logic: cohortsUrlLogic,
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import React from 'react'
|
||||
import { SceneLoading } from 'lib/utils'
|
||||
import { BindLogic, useActions, useValues } from 'kea'
|
||||
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
import { dashboardLogic, DashboardLogicProps } from 'scenes/dashboard/dashboardLogic'
|
||||
import { DashboardHeader } from 'scenes/dashboard/DashboardHeader'
|
||||
import { DashboardItems } from 'scenes/dashboard/DashboardItems'
|
||||
import { dashboardsModel } from '~/models/dashboardsModel'
|
||||
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
|
||||
import { CalendarOutlined } from '@ant-design/icons'
|
||||
import './Dashboard.scss'
|
||||
import { useKeyboardHotkeys } from '../../lib/hooks/useKeyboardHotkeys'
|
||||
import { DashboardMode } from '../../types'
|
||||
import { DashboardEventSource } from '../../lib/utils/eventUsageLogic'
|
||||
import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys'
|
||||
import { DashboardMode } from '~/types'
|
||||
import { DashboardEventSource } from 'lib/utils/eventUsageLogic'
|
||||
import { TZIndicator } from 'lib/components/TimezoneAware'
|
||||
import { EmptyDashboardComponent } from './EmptyDashboardComponent'
|
||||
import { NotFound } from 'lib/components/NotFound'
|
||||
import { DashboardReloadAction, LastRefreshText } from 'scenes/dashboard/DashboardReloadAction'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
interface Props {
|
||||
id?: string
|
||||
@ -22,7 +23,13 @@ interface Props {
|
||||
internal?: boolean
|
||||
}
|
||||
|
||||
export function Dashboard({ id, shareToken, internal }: Props): JSX.Element {
|
||||
export const scene: SceneExport = {
|
||||
component: Dashboard,
|
||||
logic: dashboardLogic,
|
||||
paramsToProps: ({ params: { id } }): DashboardLogicProps => ({ id: parseInt(id) }),
|
||||
}
|
||||
|
||||
export function Dashboard({ id, shareToken, internal }: Props = {}): JSX.Element {
|
||||
return (
|
||||
<BindLogic logic={dashboardLogic} props={{ id: id ? parseInt(id) : undefined, shareToken, internal }}>
|
||||
<DashboardView />
|
||||
|
@ -21,6 +21,13 @@ import { userLogic } from 'scenes/userLogic'
|
||||
import { ColumnType } from 'antd/lib/table'
|
||||
import { DashboardEventSource } from 'lib/utils/eventUsageLogic'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Dashboards,
|
||||
logic: sceneLogic,
|
||||
}
|
||||
|
||||
export function Dashboards(): JSX.Element {
|
||||
const { dashboardsLoading } = useValues(dashboardsModel)
|
||||
|
@ -1,58 +0,0 @@
|
||||
import React from 'react'
|
||||
import { kea, useActions, useValues } from 'kea'
|
||||
import { Tabs } from 'antd'
|
||||
import { ActionsTable } from 'scenes/actions/ActionsTable'
|
||||
import { EventsTable } from './EventsTable'
|
||||
import { EventsVolumeTable } from './EventsVolumeTable'
|
||||
import { PropertiesVolumeTable } from './PropertiesVolumeTable'
|
||||
import { eventsLogicType } from './EventsType'
|
||||
import { DefinitionDrawer } from './definitions/DefinitionDrawer'
|
||||
|
||||
const eventsLogic = kea<eventsLogicType>({
|
||||
actions: {
|
||||
setTab: (tab: string) => ({ tab }),
|
||||
},
|
||||
reducers: {
|
||||
tab: [
|
||||
'live',
|
||||
{
|
||||
setTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
},
|
||||
actionToUrl: ({ values }) => ({
|
||||
setTab: () => '/events' + (values.tab === 'live' ? '' : '/' + values.tab),
|
||||
}),
|
||||
urlToAction: ({ actions, values }) => ({
|
||||
'/events(/:tab)': ({ tab }) => {
|
||||
const currentTab = tab || 'live'
|
||||
if (currentTab !== values.tab) {
|
||||
actions.setTab(currentTab)
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
export function ManageEvents(): JSX.Element {
|
||||
const { tab } = useValues(eventsLogic)
|
||||
const { setTab } = useActions(eventsLogic)
|
||||
return (
|
||||
<div data-attr="manage-events-table" style={{ paddingTop: 32 }}>
|
||||
<Tabs tabPosition="top" animated={false} activeKey={tab} onTabClick={setTab}>
|
||||
<Tabs.TabPane tab="Events" key="live">
|
||||
<EventsTable />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span data-attr="events-actions-tab">Actions</span>} key="actions">
|
||||
<ActionsTable />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Events Stats" key="stats">
|
||||
<EventsVolumeTable />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Properties Stats" key="properties">
|
||||
<PropertiesVolumeTable />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<DefinitionDrawer />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -27,6 +27,8 @@ import { Tooltip } from 'lib/components/Tooltip'
|
||||
import { LabelledSwitch } from 'scenes/events/LabelledSwitch'
|
||||
import clsx from 'clsx'
|
||||
import { tableConfigLogic } from 'lib/components/ResizableTable/tableConfigLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { EventsTab, EventsTabs } from 'scenes/events/EventsTabs'
|
||||
|
||||
dayjs.extend(LocalizedFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
@ -43,7 +45,13 @@ interface EventsTable {
|
||||
pageKey?: string
|
||||
}
|
||||
|
||||
export function EventsTable({ fixedFilters, filtersEnabled = true, pageKey }: EventsTable): JSX.Element {
|
||||
export const scene: SceneExport = {
|
||||
component: EventsTable,
|
||||
logic: eventsTableLogic,
|
||||
paramsToProps: ({ params: { fixedFilters, pageKey } }) => ({ fixedFilters, key: pageKey }),
|
||||
}
|
||||
|
||||
export function EventsTable({ fixedFilters, filtersEnabled = true, pageKey }: EventsTable = {}): JSX.Element {
|
||||
const logic = eventsTableLogic({ fixedFilters, key: pageKey })
|
||||
|
||||
const {
|
||||
@ -283,115 +291,120 @@ export function EventsTable({ fixedFilters, filtersEnabled = true, pageKey }: Ev
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="events" data-attr="events-table">
|
||||
<PageHeader
|
||||
title="Events"
|
||||
caption="See events being sent to this project in near real time."
|
||||
style={{ marginTop: 0 }}
|
||||
/>
|
||||
<div data-attr="manage-events-table" style={{ paddingTop: 32 }}>
|
||||
<EventsTabs tab={EventsTab.Events} />
|
||||
<div className="events" data-attr="events-table">
|
||||
<PageHeader
|
||||
title="Events"
|
||||
caption="See events being sent to this project in near real time."
|
||||
style={{ marginTop: 0 }}
|
||||
/>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12}>
|
||||
<EventName
|
||||
value={eventFilter}
|
||||
onChange={(value: string) => {
|
||||
setEventFilter(value || '')
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{filtersEnabled ? <PropertyFilters pageKey={'EventsTable'} style={{ marginBottom: 0 }} /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} justify="end">
|
||||
<Col flex="1">
|
||||
<LabelledSwitch
|
||||
label={'Automatically load new events'}
|
||||
enabled={automaticLoadEnabled}
|
||||
onToggle={toggleAutomaticLoad}
|
||||
align="right"
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="0">
|
||||
{exportUrl && (
|
||||
<Tooltip title="Export up to 10,000 latest events." placement="left">
|
||||
<Button icon={<DownloadOutlined />} href={exportUrl}>
|
||||
Export events
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
{featureFlags[FEATURE_FLAGS.EVENT_COLUMN_CONFIG] && (
|
||||
<Col flex="0">
|
||||
<TableConfig
|
||||
availableColumns={propertyNames}
|
||||
immutableColumns={['event', 'person', 'when']}
|
||||
defaultColumns={defaultColumns.map((e) => e.key || '')}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12}>
|
||||
<EventName
|
||||
value={eventFilter}
|
||||
onChange={(value: string) => {
|
||||
setEventFilter(value || '')
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Col span={24}>
|
||||
{filtersEnabled ? (
|
||||
<PropertyFilters pageKey={'EventsTable'} style={{ marginBottom: 0 }} />
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div>
|
||||
<ResizableTable
|
||||
dataSource={eventsFormatted}
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
size="small"
|
||||
key={selectedColumns === 'DEFAULT' ? 'default' : selectedColumns.join('-')}
|
||||
className="ph-no-capture"
|
||||
locale={{
|
||||
emptyText: isLoading ? (
|
||||
<span> </span>
|
||||
) : (
|
||||
<span>
|
||||
You don't have any items here! If you haven't integrated PostHog yet,{' '}
|
||||
<Link to="/project/settings">click here to set PostHog up on your app</Link>.
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
|
||||
rowKey={(row) =>
|
||||
row.event ? row.event.id + '-' + row.event.event : row.date_break?.toString() || ''
|
||||
}
|
||||
rowClassName={(row) => {
|
||||
return clsx({
|
||||
'event-row': row.event,
|
||||
'highlight-new-row': row.event && highlightEvents[(row.event as EventType).id],
|
||||
'event-row-is-exception': row.event && row.event.event === '$exception',
|
||||
'event-day-separator': row.date_break,
|
||||
'event-row-new': row.new_events,
|
||||
})
|
||||
}}
|
||||
expandable={{
|
||||
expandedRowRender: function renderExpand({ event }) {
|
||||
return event && <EventDetails event={event} />
|
||||
},
|
||||
rowExpandable: ({ event }) => !!event,
|
||||
expandRowByClick: true,
|
||||
}}
|
||||
onRow={(row) => ({
|
||||
onClick: () => {
|
||||
if (row.new_events) {
|
||||
prependNewEvents(newEvents)
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
visibility: hasNext || isLoadingNext ? 'visible' : 'hidden',
|
||||
margin: '2rem auto 5rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Button type="primary" onClick={fetchNextEvents}>
|
||||
{isLoadingNext ? <Spin /> : 'Load more events'}
|
||||
</Button>
|
||||
<Row gutter={[16, 16]} justify="end">
|
||||
<Col flex="1">
|
||||
<LabelledSwitch
|
||||
label={'Automatically load new events'}
|
||||
enabled={automaticLoadEnabled}
|
||||
onToggle={toggleAutomaticLoad}
|
||||
align="right"
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="0">
|
||||
{exportUrl && (
|
||||
<Tooltip title="Export up to 10,000 latest events." placement="left">
|
||||
<Button icon={<DownloadOutlined />} href={exportUrl}>
|
||||
Export events
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
{featureFlags[FEATURE_FLAGS.EVENT_COLUMN_CONFIG] && (
|
||||
<Col flex="0">
|
||||
<TableConfig
|
||||
availableColumns={propertyNames}
|
||||
immutableColumns={['event', 'person', 'when']}
|
||||
defaultColumns={defaultColumns.map((e) => e.key || '')}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<div>
|
||||
<ResizableTable
|
||||
dataSource={eventsFormatted}
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
size="small"
|
||||
key={selectedColumns === 'DEFAULT' ? 'default' : selectedColumns.join('-')}
|
||||
className="ph-no-capture"
|
||||
locale={{
|
||||
emptyText: isLoading ? (
|
||||
<span> </span>
|
||||
) : (
|
||||
<span>
|
||||
You don't have any items here! If you haven't integrated PostHog yet,{' '}
|
||||
<Link to="/project/settings">click here to set PostHog up on your app</Link>.
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
|
||||
rowKey={(row) =>
|
||||
row.event ? row.event.id + '-' + row.event.event : row.date_break?.toString() || ''
|
||||
}
|
||||
rowClassName={(row) => {
|
||||
return clsx({
|
||||
'event-row': row.event,
|
||||
'highlight-new-row': row.event && highlightEvents[(row.event as EventType).id],
|
||||
'event-row-is-exception': row.event && row.event.event === '$exception',
|
||||
'event-day-separator': row.date_break,
|
||||
'event-row-new': row.new_events,
|
||||
})
|
||||
}}
|
||||
expandable={{
|
||||
expandedRowRender: function renderExpand({ event }) {
|
||||
return event && <EventDetails event={event} />
|
||||
},
|
||||
rowExpandable: ({ event }) => !!event,
|
||||
expandRowByClick: true,
|
||||
}}
|
||||
onRow={(row) => ({
|
||||
onClick: () => {
|
||||
if (row.new_events) {
|
||||
prependNewEvents(newEvents)
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
visibility: hasNext || isLoadingNext ? 'visible' : 'hidden',
|
||||
margin: '2rem auto 5rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Button type="primary" onClick={fetchNextEvents}>
|
||||
{isLoadingNext ? <Spin /> : 'Load more events'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '5rem' }} />
|
||||
</div>
|
||||
<div style={{ marginTop: '5rem' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
60
frontend/src/scenes/events/EventsTabs.tsx
Normal file
60
frontend/src/scenes/events/EventsTabs.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React from 'react'
|
||||
import { kea, useActions } from 'kea'
|
||||
import { Tabs } from 'antd'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { eventsTabsLogicType } from './EventsTabsType'
|
||||
|
||||
export enum EventsTab {
|
||||
Events = 'events',
|
||||
Actions = 'actions',
|
||||
EventStats = 'stats',
|
||||
EventPropertyStats = 'properties',
|
||||
}
|
||||
|
||||
const tabUrls: Record<EventsTab, string> = {
|
||||
[EventsTab.EventPropertyStats]: urls.eventPropertyStats(),
|
||||
[EventsTab.EventStats]: urls.eventStats(),
|
||||
[EventsTab.Actions]: urls.actions(),
|
||||
[EventsTab.Events]: urls.events(),
|
||||
}
|
||||
|
||||
const eventsTabsLogic = kea<eventsTabsLogicType<EventsTab>>({
|
||||
actions: {
|
||||
setTab: (tab: EventsTab) => ({ tab }),
|
||||
},
|
||||
reducers: {
|
||||
tab: [
|
||||
EventsTab.Events as EventsTab,
|
||||
{
|
||||
setTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
},
|
||||
actionToUrl: () => ({
|
||||
setTab: ({ tab }) => tabUrls[tab as EventsTab] || urls.events(),
|
||||
}),
|
||||
urlToAction: ({ actions, values }) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tabUrls).map(([key, url]) => [
|
||||
url,
|
||||
() => {
|
||||
if (values.tab !== key) {
|
||||
actions.setTab(key as EventsTab)
|
||||
}
|
||||
},
|
||||
])
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export function EventsTabs({ tab }: { tab: EventsTab }): JSX.Element {
|
||||
const { setTab } = useActions(eventsTabsLogic)
|
||||
return (
|
||||
<Tabs tabPosition="top" animated={false} activeKey={tab} onTabClick={(t) => setTab(t as EventsTab)}>
|
||||
<Tabs.TabPane tab="Events" key="events" />
|
||||
<Tabs.TabPane tab={<span data-attr="events-actions-tab">Actions</span>} key="actions" />
|
||||
<Tabs.TabPane tab="Events Stats" key="stats" />
|
||||
<Tabs.TabPane tab="Properties Stats" key="properties" />
|
||||
</Tabs>
|
||||
)
|
||||
}
|
@ -6,13 +6,22 @@ import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { eventDefinitionsModel } from '~/models/eventDefinitionsModel'
|
||||
import { UsageDisabledWarning } from './UsageDisabledWarning'
|
||||
import { VolumeTable } from './VolumeTable'
|
||||
import { DefinitionDrawer } from 'scenes/events/definitions/DefinitionDrawer'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { EventsTab, EventsTabs } from 'scenes/events/EventsTabs'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: EventsVolumeTable,
|
||||
logic: eventDefinitionsModel,
|
||||
}
|
||||
|
||||
export function EventsVolumeTable(): JSX.Element | null {
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
const { eventDefinitions, loaded } = useValues(eventDefinitionsModel)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-attr="manage-events-table" style={{ paddingTop: 32 }}>
|
||||
<EventsTabs tab={EventsTab.EventStats} />
|
||||
<PageHeader
|
||||
title="Events Stats"
|
||||
caption="See all event names that have ever been sent to this team, including the volume and how often queries where made using this event."
|
||||
@ -37,6 +46,7 @@ export function EventsVolumeTable(): JSX.Element | null {
|
||||
) : (
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
)}
|
||||
</>
|
||||
<DefinitionDrawer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -6,13 +6,23 @@ import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { UsageDisabledWarning } from './UsageDisabledWarning'
|
||||
import { VolumeTable } from './VolumeTable'
|
||||
import { DefinitionDrawer } from 'scenes/events/definitions/DefinitionDrawer'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { eventDefinitionsModel } from '~/models/eventDefinitionsModel'
|
||||
import { EventsTab, EventsTabs } from 'scenes/events/EventsTabs'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: PropertiesVolumeTable,
|
||||
logic: eventDefinitionsModel,
|
||||
}
|
||||
|
||||
export function PropertiesVolumeTable(): JSX.Element | null {
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
const { propertyDefinitions, loaded } = useValues(propertyDefinitionsModel)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-attr="manage-events-table" style={{ paddingTop: 32 }}>
|
||||
<EventsTabs tab={EventsTab.EventPropertyStats} />
|
||||
<PageHeader
|
||||
title="Properties Stats"
|
||||
caption="See all property keys that have ever been sent to this team, including the volume and how often
|
||||
@ -39,6 +49,7 @@ export function PropertiesVolumeTable(): JSX.Element | null {
|
||||
) : (
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
)}
|
||||
</>
|
||||
<DefinitionDrawer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Meta } from '@storybook/react'
|
||||
import { keaStory } from 'lib/storybook/kea-story'
|
||||
|
||||
import { ManageEvents } from '../Events'
|
||||
|
||||
import eventsState from './events.json'
|
||||
import { EventsTable } from 'scenes/events'
|
||||
|
||||
export default {
|
||||
title: 'PostHog/Scenes/Events',
|
||||
} as Meta
|
||||
|
||||
export const AllEvents = keaStory(ManageEvents, eventsState)
|
||||
export const AllEvents = keaStory(EventsTable, eventsState)
|
||||
|
@ -1,5 +1,5 @@
|
||||
export * from './EventDetails'
|
||||
export * from './Events'
|
||||
export * from './EventsTabs'
|
||||
export * from './EventElements'
|
||||
export * from './EventsTable'
|
||||
export * from './eventsTableLogic'
|
||||
|
@ -28,6 +28,12 @@ import { IconExternalLink, IconJavascript, IconPython } from 'lib/components/ico
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { Tooltip } from 'lib/components/Tooltip'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: FeatureFlag,
|
||||
logic: featureFlagLogic,
|
||||
}
|
||||
|
||||
const UTM_TAGS = '?utm_medium=in-product&utm_campaign=feature-flag'
|
||||
|
||||
|
@ -17,6 +17,12 @@ import { urls } from 'scenes/urls'
|
||||
import { Tooltip } from 'lib/components/Tooltip'
|
||||
import stringWithWBR from 'lib/utils/stringWithWBR'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: FeatureFlags,
|
||||
logic: featureFlagsLogic,
|
||||
}
|
||||
|
||||
export function FeatureFlags(): JSX.Element {
|
||||
const { currentTeamId } = useValues(teamLogic)
|
||||
|
@ -311,7 +311,7 @@ export const funnelLogic = kea<funnelLogicType>({
|
||||
0
|
||||
)
|
||||
}
|
||||
return people.sort((a, b) => score(b) - score(a))
|
||||
return [...people].sort((a, b) => score(b) - score(a))
|
||||
},
|
||||
],
|
||||
isStepsEmpty: [() => [selectors.filters], (filters: FilterType) => isStepsEmpty(filters)],
|
||||
|
@ -13,6 +13,12 @@ import { FrameworkPanel } from 'scenes/ingestion/panels/FrameworkPanel'
|
||||
import { FrameworkGrid } from 'scenes/ingestion/panels/FrameworkGrid'
|
||||
import { PlatformPanel } from 'scenes/ingestion/panels/PlatformPanel'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: IngestionWizard,
|
||||
logic: ingestionLogic,
|
||||
}
|
||||
|
||||
export function IngestionContainer({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
return (
|
||||
|
@ -127,7 +127,7 @@ describe('insightMetadataLogic', () => {
|
||||
.toDispatchActions(insightLogic, [
|
||||
insightLogic({ dashboardItemId: undefined }).actionCreators.setInsight(
|
||||
{ name: insight.name },
|
||||
true
|
||||
{ shouldMergeWithExisting: true }
|
||||
),
|
||||
])
|
||||
.toDispatchActions(logic, [
|
||||
|
@ -64,7 +64,10 @@ export const insightMetadataLogic = kea<insightMetadataLogicType<InsightMetadata
|
||||
await actions.updateInsight({ [property]: values.insightMetadata[property] })
|
||||
} else {
|
||||
// Update local insight state
|
||||
await actions.setInsight({ [property]: values.insightMetadata[property] }, true)
|
||||
await actions.setInsight(
|
||||
{ [property]: values.insightMetadata[property] },
|
||||
{ shouldMergeWithExisting: true }
|
||||
)
|
||||
}
|
||||
actions.setInsightMetadata({ [property]: values.insightMetadata[property] }) // sync
|
||||
actions.showViewMode(property)
|
||||
|
@ -8,6 +8,7 @@ import React from 'react'
|
||||
import { DashboardItemType } from '~/types'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { insightRouterLogicType } from './InsightRouterType'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
const insightRouterLogic = kea<insightRouterLogicType>({
|
||||
actions: {
|
||||
@ -28,7 +29,7 @@ const insightRouterLogic = kea<insightRouterLogicType>({
|
||||
if (response.results.length) {
|
||||
const item = response.results[0] as DashboardItemType
|
||||
eventUsageLogic.actions.reportInsightShortUrlVisited(true, item.filters.insight || null)
|
||||
router.actions.push(
|
||||
router.actions.replace(
|
||||
combineUrl('/insights', item.filters, {
|
||||
fromItem: item.id,
|
||||
fromItemName: item.name,
|
||||
@ -79,3 +80,8 @@ export function InsightRouter(): JSX.Element {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: InsightRouter,
|
||||
logic: insightRouterLogic,
|
||||
}
|
||||
|
@ -23,9 +23,16 @@ import { InsightsNav } from './InsightsNav'
|
||||
import { SaveToDashboard } from 'lib/components/SaveToDashboard/SaveToDashboard'
|
||||
import { InsightContainer } from 'scenes/insights/InsightContainer'
|
||||
import { InsightMetadata } from 'scenes/insights/InsightMetadata'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Insights,
|
||||
logic: insightLogic,
|
||||
paramsToProps: ({ hashParams: { fromItem } }) => ({ dashboardItemId: fromItem, syncWithUrl: true }),
|
||||
}
|
||||
|
||||
export function Insights(): JSX.Element {
|
||||
useMountedLogic(insightCommandLogic)
|
||||
const {
|
||||
@ -81,7 +88,7 @@ export function Insights(): JSX.Element {
|
||||
},
|
||||
})
|
||||
|
||||
const scene = (
|
||||
const insightScene = (
|
||||
<div className="insights-page">
|
||||
{featureFlags[FEATURE_FLAGS.SAVED_INSIGHTS] && insightMode === ItemMode.View ? (
|
||||
<div className="insight-metadata">
|
||||
@ -253,7 +260,7 @@ export function Insights(): JSX.Element {
|
||||
|
||||
return (
|
||||
<BindLogic logic={insightLogic} props={insightProps}>
|
||||
{scene}
|
||||
{insightScene}
|
||||
</BindLogic>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { defaultAPIMocks, mockAPI, MOCK_TEAM_ID } from 'lib/api.mock'
|
||||
import { expectLogic } from 'kea-test-utils'
|
||||
import { expectLogic, partial } from 'kea-test-utils'
|
||||
import { initKeaTestLogic } from '~/test/init'
|
||||
import { insightLogic } from './insightLogic'
|
||||
import { AvailableFeature, PropertyOperator, ViewType } from '~/types'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { combineUrl, router } from 'kea-router'
|
||||
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
jest.mock('lib/api')
|
||||
|
||||
@ -28,6 +31,14 @@ describe('insightLogic', () => {
|
||||
properties: [{ value: 'a', operator: PropertyOperator.Exact, key: 'a', type: 'a' }],
|
||||
},
|
||||
}
|
||||
} else if ([`api/projects/${MOCK_TEAM_ID}/dashboards/33/`].includes(pathname)) {
|
||||
return {
|
||||
id: 33,
|
||||
filters: {},
|
||||
items: [
|
||||
{ id: 42, result: 'result!', filters: { insight: 'TRENDS', interval: 'month' }, tags: ['bla'] },
|
||||
],
|
||||
}
|
||||
} else if ([`api/projects/${MOCK_TEAM_ID}/insights/44`].includes(pathname)) {
|
||||
throwAPIError()
|
||||
} else if (
|
||||
@ -374,4 +385,33 @@ describe('insightLogic', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('takes data from dashboardLogic if available', () => {
|
||||
initKeaTestLogic()
|
||||
it('works if all conditions match', async () => {
|
||||
// 0. the feature flag must be set
|
||||
featureFlagLogic.mount()
|
||||
featureFlagLogic.actions.setFeatureFlags([FEATURE_FLAGS.TURBO_MODE], { [FEATURE_FLAGS.TURBO_MODE]: true })
|
||||
|
||||
// 1. the URL must have the dashboard and insight IDs
|
||||
router.actions.push('/insights', {}, { fromDashboard: 33, fromItem: 42 })
|
||||
|
||||
// 2. the dashboard is mounted
|
||||
const dashLogic = dashboardLogic({ id: 33 })
|
||||
dashLogic.mount()
|
||||
await expectLogic(dashLogic).toDispatchActions(['loadDashboardItemsSuccess'])
|
||||
|
||||
// 3. mount the insight
|
||||
logic = insightLogic({ dashboardItemId: 42 })
|
||||
logic.mount()
|
||||
|
||||
// 4. verify it didn't make any API calls
|
||||
await expectLogic(logic)
|
||||
.toDispatchActions(['setInsight'])
|
||||
.toNotHaveDispatchedActions(['setFilters', 'loadResults', 'loadInsight', 'updateInsight'])
|
||||
.toMatchValues({
|
||||
insight: partial({ id: 42, result: 'result!', filters: { insight: 'TRENDS', interval: 'month' } }),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -3,9 +3,17 @@ import { errorToast, objectsEqual, toParams, uuid } from 'lib/utils'
|
||||
import posthog from 'posthog-js'
|
||||
import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic'
|
||||
import { insightLogicType } from './insightLogicType'
|
||||
import { DashboardItemType, FilterType, InsightLogicProps, InsightType, ItemMode, ViewType } from '~/types'
|
||||
import {
|
||||
DashboardItemType,
|
||||
FilterType,
|
||||
InsightLogicProps,
|
||||
InsightType,
|
||||
ItemMode,
|
||||
SetInsightOptions,
|
||||
ViewType,
|
||||
} from '~/types'
|
||||
import { captureInternalMetric } from 'lib/internalMetrics'
|
||||
import { Scene, sceneLogic } from 'scenes/sceneLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { router } from 'kea-router'
|
||||
import api from 'lib/api'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -21,6 +29,8 @@ import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { extractObjectDiffKeys } from './utils'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
|
||||
const IS_TEST_MODE = process.env.NODE_ENV === 'test'
|
||||
|
||||
@ -40,7 +50,7 @@ export const insightLogic = kea<insightLogicType>({
|
||||
|
||||
connect: {
|
||||
values: [teamLogic, ['currentTeamId']],
|
||||
logic: [eventUsageLogic, dashboardsModel],
|
||||
logic: [eventUsageLogic, dashboardsModel, featureFlagLogic],
|
||||
},
|
||||
|
||||
actions: () => ({
|
||||
@ -70,9 +80,9 @@ export const insightLogic = kea<insightLogicType>({
|
||||
toggleControlsCollapsed: true,
|
||||
saveNewTag: (tag: string) => ({ tag }),
|
||||
deleteTag: (tag: string) => ({ tag }),
|
||||
setInsight: (insight: Partial<DashboardItemType>, shouldMergeWithExisting: boolean = false) => ({
|
||||
setInsight: (insight: Partial<DashboardItemType>, options: SetInsightOptions = {}) => ({
|
||||
insight,
|
||||
shouldMergeWithExisting,
|
||||
options,
|
||||
}),
|
||||
setInsightMode: (mode: ItemMode, source: InsightEventSource | null) => ({ mode, source }),
|
||||
setInsightDescription: (description: string) => ({ description }),
|
||||
@ -235,13 +245,10 @@ export const insightLogic = kea<insightLogicType>({
|
||||
result: null,
|
||||
filters: {},
|
||||
},
|
||||
setInsight: (state, { insight, shouldMergeWithExisting }) =>
|
||||
shouldMergeWithExisting
|
||||
? {
|
||||
...state,
|
||||
...insight,
|
||||
}
|
||||
: insight,
|
||||
setInsight: (state, { insight, options: { shouldMergeWithExisting } }) => ({
|
||||
...(shouldMergeWithExisting ? state : {}),
|
||||
...insight,
|
||||
}),
|
||||
updateInsightFilters: (state, { filters }) => ({ ...state, filters }),
|
||||
},
|
||||
showTimeoutMessage: [false, { setShowTimeoutMessage: (_, { showTimeoutMessage }) => showTimeoutMessage }],
|
||||
@ -283,6 +290,8 @@ export const insightLogic = kea<insightLogicType>({
|
||||
() => props.filters || ({} as Partial<FilterType>),
|
||||
{
|
||||
setFilters: (state, { filters }) => cleanFilters(filters, state),
|
||||
setInsight: (state, { insight: { filters }, options: { overrideFilter } }) =>
|
||||
overrideFilter ? cleanFilters(filters || {}) : state,
|
||||
loadInsightSuccess: (state, { insight }) =>
|
||||
Object.keys(state).length === 0 && insight.filters ? insight.filters : state,
|
||||
loadResultsSuccess: (state, { insight }) =>
|
||||
@ -568,19 +577,39 @@ export const insightLogic = kea<insightLogicType>({
|
||||
urlToAction: ({ actions, values, props }) => ({
|
||||
'/insights': (_: any, searchParams: Record<string, any>, hashParams: Record<string, any>) => {
|
||||
if (props.syncWithUrl) {
|
||||
let loadedFromDashboard = false
|
||||
if (searchParams.insight === 'HISTORY' || !hashParams.fromItem) {
|
||||
if (values.insightMode !== ItemMode.Edit) {
|
||||
actions.setInsightMode(ItemMode.Edit, null)
|
||||
}
|
||||
} else if (hashParams.fromItem) {
|
||||
if (!values.insight?.id || values.insight?.id !== hashParams.fromItem) {
|
||||
const insightIdChanged = !values.insight.id || values.insight.id !== hashParams.fromItem
|
||||
|
||||
if (
|
||||
featureFlagLogic.values.featureFlags[FEATURE_FLAGS.TURBO_MODE] &&
|
||||
hashParams.fromDashboard &&
|
||||
(!values.insight.result || insightIdChanged)
|
||||
) {
|
||||
const logic = dashboardLogic.findMounted({ id: hashParams.fromDashboard })
|
||||
if (logic) {
|
||||
const insight = logic.values.allItems?.items?.find(
|
||||
(item: DashboardItemType) => item.id === Number(hashParams.fromItem)
|
||||
)
|
||||
if (insight?.result) {
|
||||
actions.setInsight(insight, { overrideFilter: true })
|
||||
loadedFromDashboard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!loadedFromDashboard && insightIdChanged) {
|
||||
// Do not load the result if missing, as setFilters below will do so anyway.
|
||||
actions.loadInsight(hashParams.fromItem, { doNotLoadResults: true })
|
||||
}
|
||||
}
|
||||
|
||||
const cleanSearchParams = cleanFilters(searchParams, values.filters)
|
||||
if (!objectsEqual(cleanSearchParams, values.filters)) {
|
||||
if (!loadedFromDashboard && !objectsEqual(cleanSearchParams, values.filters)) {
|
||||
actions.setFilters(cleanSearchParams)
|
||||
}
|
||||
}
|
||||
@ -590,6 +619,18 @@ export const insightLogic = kea<insightLogicType>({
|
||||
afterMount: () => {
|
||||
if (!props.cachedResults) {
|
||||
if (props.dashboardItemId && !props.filters) {
|
||||
if (
|
||||
featureFlagLogic.values.featureFlags[FEATURE_FLAGS.TURBO_MODE] &&
|
||||
router.values.hashParams.fromItem === props.dashboardItemId &&
|
||||
router.values.hashParams.fromDashboard
|
||||
) {
|
||||
const logic = dashboardLogic.findMounted({ id: router.values.hashParams.fromDashboard })
|
||||
const insight = logic?.values.allItems?.items?.find((item) => item.id === props.dashboardItemId)
|
||||
if (insight?.result) {
|
||||
actions.setInsight(insight, { overrideFilter: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
actions.loadInsight(props.dashboardItemId)
|
||||
} else if (!props.doNotLoad) {
|
||||
actions.loadResults()
|
||||
|
@ -7,6 +7,12 @@ import { CodeSnippet } from 'scenes/ingestion/frameworks/CodeSnippet'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Tooltip } from 'lib/components/Tooltip'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Licenses,
|
||||
logic: licenseLogic,
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -9,6 +9,12 @@ import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { IconExternalLink } from 'lib/components/icons'
|
||||
import { OverviewTab } from 'scenes/instance/SystemStatus/OverviewTab'
|
||||
import { InternalMetricsTab } from 'scenes/instance/SystemStatus/InternalMetricsTab'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: SystemStatus,
|
||||
logic: systemStatusLogic,
|
||||
}
|
||||
|
||||
export function SystemStatus(): JSX.Element {
|
||||
const { tab, error, systemStatus } = useValues(systemStatusLogic)
|
||||
|
@ -8,6 +8,11 @@ import { ChangePassword } from './ChangePassword'
|
||||
import { PersonalAPIKeys } from 'lib/components/PersonalAPIKeys'
|
||||
import { OptOutCapture } from './OptOutCapture'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: MySettings,
|
||||
}
|
||||
|
||||
export function MySettings(): JSX.Element {
|
||||
const { location } = useValues(router)
|
||||
|
@ -23,9 +23,15 @@ import { LinkButton } from 'lib/components/LinkButton'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
const { Panel } = Collapse
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: OnboardingSetup,
|
||||
logic: onboardingSetupLogic,
|
||||
}
|
||||
|
||||
function PanelHeader({
|
||||
title,
|
||||
caption,
|
||||
|
@ -6,6 +6,12 @@ import { RadioSelect } from 'lib/components/RadioSelect'
|
||||
import { ROLES, PRODUCTS, IS_TECHNICAL } from './personalizationOptions'
|
||||
import { Link } from 'lib/components/Link'
|
||||
import './Personalization.scss'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Personalization,
|
||||
logic: personalizationLogic,
|
||||
}
|
||||
|
||||
export function Personalization(): JSX.Element {
|
||||
const { personalizationData } = useValues(personalizationLogic)
|
||||
|
@ -1,5 +1,12 @@
|
||||
import React from 'react'
|
||||
import { CreateOrganizationModal } from '../CreateOrganizationModal'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Create,
|
||||
logic: organizationLogic,
|
||||
}
|
||||
|
||||
export function Create(): JSX.Element {
|
||||
return <CreateOrganizationModal isVisible />
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Button, Card, Input, Divider, Select, Skeleton, Switch } from 'antd'
|
||||
import { UserType } from '~/types'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { Invites } from './Invites'
|
||||
import { Members } from './Members'
|
||||
@ -11,6 +10,13 @@ import { RestrictedArea, RestrictedComponentProps } from '../../../lib/component
|
||||
import { OrganizationMembershipLevel } from '../../../lib/constants'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { IconExternalLink } from 'lib/components/icons'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: OrganizationSettings,
|
||||
logic: organizationLogic,
|
||||
}
|
||||
|
||||
function DisplayName({ isRestricted }: RestrictedComponentProps): JSX.Element {
|
||||
const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic)
|
||||
@ -137,7 +143,8 @@ function EmailPreferences({ isRestricted }: RestrictedComponentProps): JSX.Eleme
|
||||
)
|
||||
}
|
||||
|
||||
export function OrganizationSettings({ user }: { user: UserType }): JSX.Element {
|
||||
export function OrganizationSettings(): JSX.Element {
|
||||
const { user } = useValues(userLogic)
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
|
||||
return (
|
||||
@ -160,7 +167,7 @@ export function OrganizationSettings({ user }: { user: UserType }): JSX.Element
|
||||
)}
|
||||
<Invites />
|
||||
<Divider />
|
||||
<Members user={user} />
|
||||
{user && <Members user={user} />}
|
||||
<Divider />
|
||||
<RestrictedArea Component={EmailPreferences} minimumAccessLevel={OrganizationMembershipLevel.Admin} />
|
||||
<Divider />
|
||||
|
@ -29,10 +29,17 @@ import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { SplitPerson } from './SplitPerson'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Person,
|
||||
logic: personsLogic,
|
||||
}
|
||||
|
||||
export function Person(): JSX.Element {
|
||||
const [activeCardTab, setActiveCardTab] = useState('properties')
|
||||
const [mergeModalOpen, setMergeModalOpen] = useState(false)
|
||||
|
@ -12,8 +12,18 @@ import { ClockCircleFilled } from '@ant-design/icons'
|
||||
import { toParams } from 'lib/utils'
|
||||
import { PersonsSearch } from './PersonsSearch'
|
||||
import { IconExternalLink } from 'lib/components/icons'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export function Persons({ cohort }: { cohort: CohortType }): JSX.Element {
|
||||
export const scene: SceneExport = {
|
||||
component: Persons,
|
||||
logic: personsLogic,
|
||||
}
|
||||
|
||||
interface PersonsProps {
|
||||
cohort?: CohortType
|
||||
}
|
||||
|
||||
export function Persons({ cohort }: PersonsProps = {}): JSX.Element {
|
||||
const { loadPersons, setListFilters } = useActions(personsLogic)
|
||||
const { persons, listFilters, personsLoading } = useValues(personsLogic)
|
||||
|
||||
|
@ -10,20 +10,27 @@ import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { PluginTab } from 'scenes/plugins/types'
|
||||
import { AdvancedTab } from 'scenes/plugins/tabs/advanced/AdvancedTab'
|
||||
import { canGloballyManagePlugins, canInstallPlugins, canViewPlugins } from './access'
|
||||
import { UserType } from '../../types'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export function Plugins({ user }: { user: UserType }): JSX.Element | null {
|
||||
export const scene: SceneExport = {
|
||||
component: Plugins,
|
||||
logic: pluginsLogic,
|
||||
}
|
||||
|
||||
export function Plugins(): JSX.Element | null {
|
||||
const { user } = useValues(userLogic)
|
||||
const { pluginTab } = useValues(pluginsLogic)
|
||||
const { setPluginTab } = useActions(pluginsLogic)
|
||||
const { TabPane } = Tabs
|
||||
|
||||
useEffect(() => {
|
||||
if (!canViewPlugins(user.organization)) {
|
||||
if (!canViewPlugins(user?.organization)) {
|
||||
window.location.href = '/'
|
||||
}
|
||||
}, [user])
|
||||
|
||||
if (!canViewPlugins(user.organization)) {
|
||||
if (!user || !canViewPlugins(user?.organization)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,10 @@
|
||||
import React from 'react'
|
||||
import { CreateProjectModal } from '../CreateProjectModal'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Create,
|
||||
}
|
||||
|
||||
export function Create(): JSX.Element {
|
||||
return <CreateProjectModal isVisible />
|
||||
|
@ -23,12 +23,17 @@ import { TestAccountFiltersConfig } from './TestAccountFiltersConfig'
|
||||
import { TimezoneConfig } from './TimezoneConfig'
|
||||
import { DataAttributes } from 'scenes/project/Settings/DataAttributes'
|
||||
import { featureFlagLogic } from '../../../lib/logic/featureFlagLogic'
|
||||
import { AvailableFeature, UserType } from '../../../types'
|
||||
import { AvailableFeature } from '../../../types'
|
||||
import { TeamMembers } from './TeamMembers'
|
||||
import { teamMembersLogic } from './teamMembersLogic'
|
||||
import { AccessControl } from './AccessControl'
|
||||
import { PathCleaningFiltersConfig } from './PathCleaningFiltersConfig'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: ProjectSettings,
|
||||
}
|
||||
|
||||
function DisplayName(): JSX.Element {
|
||||
const { currentTeam, currentTeamLoading } = useValues(teamLogic)
|
||||
@ -69,12 +74,12 @@ function DisplayName(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
export function ProjectSettings({ user }: { user: UserType }): JSX.Element {
|
||||
export function ProjectSettings(): JSX.Element {
|
||||
const { currentTeam, currentTeamLoading } = useValues(teamLogic)
|
||||
const { resetToken } = useActions(teamLogic)
|
||||
const { location } = useValues(router)
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
const { hasAvailableFeature } = useValues(userLogic)
|
||||
const { user, hasAvailableFeature } = useValues(userLogic)
|
||||
|
||||
useAnchor(location.hash)
|
||||
|
||||
@ -264,7 +269,7 @@ export function ProjectSettings({ user }: { user: UserType }): JSX.Element {
|
||||
<Divider />
|
||||
{currentTeam?.access_control && hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && (
|
||||
<BindLogic logic={teamMembersLogic} props={{ team: currentTeam }}>
|
||||
<TeamMembers user={user} team={currentTeam} />
|
||||
{user && <TeamMembers user={user} team={currentTeam} />}
|
||||
<Divider />
|
||||
</BindLogic>
|
||||
)}
|
||||
|
@ -36,6 +36,7 @@ import dayjs from 'dayjs'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { SavedInsightsEmptyState } from 'scenes/insights/EmptyStates'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
@ -49,6 +50,11 @@ export interface InsightItem {
|
||||
description: string
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: SavedInsights,
|
||||
logic: savedInsightsLogic,
|
||||
}
|
||||
|
||||
export function SavedInsights(): JSX.Element {
|
||||
const {
|
||||
loadInsights,
|
||||
|
82
frontend/src/scenes/sceneLogic.test.ts
Normal file
82
frontend/src/scenes/sceneLogic.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { sceneLogic } from './sceneLogic'
|
||||
import { initKeaTests } from '~/test/init'
|
||||
import { expectLogic, partial, truth } from 'kea-test-utils'
|
||||
import { LoadedScene, Scene } from 'scenes/sceneTypes'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { defaultAPIMocks, mockAPI } from 'lib/api.mock'
|
||||
import { router } from 'kea-router'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
jest.mock('lib/api')
|
||||
|
||||
const expectedAnnotation = partial({
|
||||
name: Scene.Annotations,
|
||||
component: expect.any(Function),
|
||||
logic: expect.any(Function),
|
||||
sceneParams: { hashParams: {}, params: {}, searchParams: {} },
|
||||
lastTouch: expect.any(Number),
|
||||
})
|
||||
|
||||
const expectedDashboard = partial({
|
||||
name: Scene.Dashboard,
|
||||
component: expect.any(Function),
|
||||
logic: expect.any(Function),
|
||||
sceneParams: { hashParams: {}, params: { id: '123' }, searchParams: {} },
|
||||
lastTouch: expect.any(Number),
|
||||
})
|
||||
|
||||
describe('sceneLogic', () => {
|
||||
let logic: ReturnType<typeof sceneLogic.build>
|
||||
|
||||
mockAPI(defaultAPIMocks)
|
||||
|
||||
beforeEach(async () => {
|
||||
initKeaTests()
|
||||
teamLogic.mount()
|
||||
await expectLogic(teamLogic).toDispatchActions(['loadCurrentTeamSuccess'])
|
||||
featureFlagLogic.mount()
|
||||
router.actions.push(urls.annotations())
|
||||
logic = sceneLogic()
|
||||
logic.mount()
|
||||
})
|
||||
|
||||
it('has preloaded some scenes', async () => {
|
||||
const preloadedScenes = [Scene.Error404, Scene.ErrorNetwork, Scene.ErrorProjectUnavailable]
|
||||
await expectLogic(logic).toMatchValues({
|
||||
loadedScenes: truth(
|
||||
(obj: Record<string, LoadedScene>) =>
|
||||
Object.keys(obj).filter((key) => preloadedScenes.includes(key as Scene)).length === 3
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it('changing URL runs openScene, loadScene and setScene', async () => {
|
||||
await expectLogic(logic).toDispatchActions(['openScene', 'loadScene', 'setScene']).toMatchValues({
|
||||
scene: Scene.Annotations,
|
||||
})
|
||||
router.actions.push(urls.dashboard(123))
|
||||
await expectLogic(logic).toDispatchActions(['openScene', 'loadScene', 'setScene']).toMatchValues({
|
||||
scene: Scene.Dashboard,
|
||||
})
|
||||
})
|
||||
|
||||
it('persists the loaded scenes', async () => {
|
||||
await expectLogic(logic)
|
||||
.delay(1)
|
||||
.toMatchValues({
|
||||
loadedScenes: partial({
|
||||
[Scene.Annotations]: expectedAnnotation,
|
||||
}),
|
||||
})
|
||||
router.actions.push(urls.dashboard(123))
|
||||
await expectLogic(logic)
|
||||
.delay(1)
|
||||
.toMatchValues({
|
||||
loadedScenes: partial({
|
||||
[Scene.Annotations]: expectedAnnotation,
|
||||
[Scene.Dashboard]: expectedDashboard,
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
@ -1,8 +1,6 @@
|
||||
import { kea, LogicWrapper } from 'kea'
|
||||
import { kea } from 'kea'
|
||||
import { router } from 'kea-router'
|
||||
import { identifierToHuman, delay } from 'lib/utils'
|
||||
import { Error404 as Error404Component } from '~/layout/Error404'
|
||||
import { ErrorNetwork as ErrorNetworkComponent } from '~/layout/ErrorNetwork'
|
||||
import { identifierToHuman } from 'lib/utils'
|
||||
import posthog from 'posthog-js'
|
||||
import { sceneLogicType } from './sceneLogicType'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
@ -10,293 +8,25 @@ import { preflightLogic } from './PreflightCheck/logic'
|
||||
import { AvailableFeature } from '~/types'
|
||||
import { userLogic } from './userLogic'
|
||||
import { afterLoginRedirect } from './authentication/loginLogic'
|
||||
import { ErrorProjectUnavailable as ErrorProjectUnavailableComponent } from '../layout/ErrorProjectUnavailable'
|
||||
import { teamLogic } from './teamLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { SceneExport, Params, Scene, SceneConfig, SceneParams, LoadedScene } from 'scenes/sceneTypes'
|
||||
import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations, scenes } from 'scenes/scenes'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
export enum Scene {
|
||||
Error404 = '404',
|
||||
ErrorNetwork = '4xx',
|
||||
ErrorProjectUnavailable = 'projectUnavailable',
|
||||
Dashboards = 'dashboards',
|
||||
Dashboard = 'dashboard',
|
||||
Insights = 'insights',
|
||||
InsightRouter = 'insightRouter',
|
||||
Cohorts = 'cohorts',
|
||||
Events = 'events',
|
||||
Sessions = 'sessions',
|
||||
SessionRecordings = 'sessionRecordings',
|
||||
Person = 'person',
|
||||
Persons = 'persons',
|
||||
Action = 'action',
|
||||
FeatureFlags = 'featureFlags',
|
||||
FeatureFlag = 'featureFlag',
|
||||
OrganizationSettings = 'organizationSettings',
|
||||
OrganizationCreateFirst = 'organizationCreateFirst',
|
||||
ProjectSettings = 'projectSettings',
|
||||
ProjectCreateFirst = 'projectCreateFirst',
|
||||
SystemStatus = 'systemStatus',
|
||||
InstanceLicenses = 'instanceLicenses',
|
||||
MySettings = 'mySettings',
|
||||
Annotations = 'annotations',
|
||||
Billing = 'billing',
|
||||
Plugins = 'plugins',
|
||||
SavedInsights = 'savedInsights',
|
||||
// Authentication & onboarding routes
|
||||
Login = 'login',
|
||||
Signup = 'signup',
|
||||
InviteSignup = 'inviteSignup',
|
||||
PasswordReset = 'passwordReset',
|
||||
PasswordResetComplete = 'passwordResetComplete',
|
||||
PreflightCheck = 'preflightCheck',
|
||||
Ingestion = 'ingestion',
|
||||
OnboardingSetup = 'onboardingSetup',
|
||||
Personalization = 'personalization',
|
||||
}
|
||||
|
||||
const preloadedScenes: Record<string, LoadedScene> = {
|
||||
[Scene.Error404]: {
|
||||
component: Error404Component,
|
||||
},
|
||||
[Scene.ErrorNetwork]: {
|
||||
component: ErrorNetworkComponent,
|
||||
},
|
||||
[Scene.ErrorProjectUnavailable]: {
|
||||
component: ErrorProjectUnavailableComponent,
|
||||
},
|
||||
}
|
||||
|
||||
export const scenes: Record<Scene, () => any> = {
|
||||
[Scene.Error404]: () => ({ default: preloadedScenes[Scene.Error404].component }),
|
||||
[Scene.ErrorNetwork]: () => ({ default: preloadedScenes[Scene.ErrorNetwork].component }),
|
||||
[Scene.ErrorProjectUnavailable]: () => ({ default: preloadedScenes[Scene.ErrorProjectUnavailable].component }),
|
||||
[Scene.Dashboards]: () => import(/* webpackChunkName: 'dashboards' */ './dashboard/Dashboards'),
|
||||
[Scene.Dashboard]: () => import(/* webpackChunkName: 'dashboard' */ './dashboard/Dashboard'),
|
||||
[Scene.Insights]: () => import(/* webpackChunkName: 'insights' */ './insights/Insights'),
|
||||
[Scene.InsightRouter]: () => import(/* webpackChunkName: 'insightRouter' */ './insights/InsightRouter'),
|
||||
[Scene.Cohorts]: () => import(/* webpackChunkName: 'cohorts' */ './cohorts/Cohorts'),
|
||||
[Scene.Events]: () => import(/* webpackChunkName: 'events' */ './events/Events'),
|
||||
[Scene.Sessions]: () => import(/* webpackChunkName: 'sessions' */ './sessions/Sessions'),
|
||||
[Scene.SessionRecordings]: () =>
|
||||
import(/* webpackChunkName: 'sessionRecordings' */ './session-recordings/SessionRecordings'),
|
||||
[Scene.Person]: () => import(/* webpackChunkName: 'person' */ './persons/Person'),
|
||||
[Scene.Persons]: () => import(/* webpackChunkName: 'persons' */ './persons/Persons'),
|
||||
[Scene.Action]: () => import(/* webpackChunkName: 'action' */ './actions/Action'),
|
||||
[Scene.FeatureFlags]: () => import(/* webpackChunkName: 'featureFlags' */ './experimentation/FeatureFlags'),
|
||||
[Scene.FeatureFlag]: () => import(/* webpackChunkName: 'featureFlag' */ './experimentation/FeatureFlag'),
|
||||
[Scene.OrganizationSettings]: () =>
|
||||
import(/* webpackChunkName: 'organizationSettings' */ './organization/Settings'),
|
||||
[Scene.OrganizationCreateFirst]: () =>
|
||||
import(/* webpackChunkName: 'organizationCreateFirst' */ './organization/Create'),
|
||||
[Scene.ProjectSettings]: () => import(/* webpackChunkName: 'projectSettings' */ './project/Settings'),
|
||||
[Scene.ProjectCreateFirst]: () => import(/* webpackChunkName: 'projectCreateFirst' */ './project/Create'),
|
||||
[Scene.SystemStatus]: () => import(/* webpackChunkName: 'systemStatus' */ './instance/SystemStatus'),
|
||||
[Scene.InstanceLicenses]: () => import(/* webpackChunkName: 'instanceLicenses' */ './instance/Licenses'),
|
||||
[Scene.MySettings]: () => import(/* webpackChunkName: 'mySettings' */ './me/Settings'),
|
||||
[Scene.Annotations]: () => import(/* webpackChunkName: 'annotations' */ './annotations'),
|
||||
[Scene.PreflightCheck]: () => import(/* webpackChunkName: 'preflightCheck' */ './PreflightCheck'),
|
||||
[Scene.Signup]: () => import(/* webpackChunkName: 'signup' */ './authentication/Signup'),
|
||||
[Scene.InviteSignup]: () => import(/* webpackChunkName: 'inviteSignup' */ './authentication/InviteSignup'),
|
||||
[Scene.Ingestion]: () => import(/* webpackChunkName: 'ingestion' */ './ingestion/IngestionWizard'),
|
||||
[Scene.Billing]: () => import(/* webpackChunkName: 'billing' */ './billing/Billing'),
|
||||
[Scene.Plugins]: () => import(/* webpackChunkName: 'plugins' */ './plugins/Plugins'),
|
||||
[Scene.Personalization]: () => import(/* webpackChunkName: 'personalization' */ './onboarding/Personalization'),
|
||||
[Scene.OnboardingSetup]: () => import(/* webpackChunkName: 'onboardingSetup' */ './onboarding/OnboardingSetup'),
|
||||
[Scene.Login]: () => import(/* webpackChunkName: 'login' */ './authentication/Login'),
|
||||
[Scene.SavedInsights]: () => import(/* webpackChunkName: 'savedInsights' */ './saved-insights/SavedInsights'),
|
||||
[Scene.PasswordReset]: () => import(/* webpackChunkName: 'passwordReset' */ './authentication/PasswordReset'),
|
||||
[Scene.PasswordResetComplete]: () =>
|
||||
import(/* webpackChunkName: 'passwordResetComplete' */ './authentication/PasswordResetComplete'),
|
||||
}
|
||||
|
||||
interface LoadedScene {
|
||||
component: () => JSX.Element
|
||||
logic?: LogicWrapper
|
||||
}
|
||||
|
||||
interface Params {
|
||||
[param: string]: any
|
||||
}
|
||||
|
||||
interface SceneConfig {
|
||||
/** Route should only be accessed when logged out (N.B. should be added to posthog/urls.py too) */
|
||||
onlyUnauthenticated?: boolean
|
||||
/** Route **can** be accessed when logged out (i.e. can be accessed when logged in too; should be added to posthog/urls.py too) */
|
||||
allowUnauthenticated?: boolean
|
||||
/** Background is $bg_mid */
|
||||
dark?: boolean
|
||||
/** Only keeps the main content and the top navigation bar */
|
||||
plain?: boolean
|
||||
/** Hides the top navigation bar (regardless of whether `plain` is `true` or not) */
|
||||
hideTopNav?: boolean
|
||||
/** Hides demo project warnings (DemoWarning.tsx) */
|
||||
hideDemoWarnings?: boolean
|
||||
/** Route requires project access */
|
||||
projectBased?: boolean
|
||||
}
|
||||
|
||||
export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
|
||||
// Project-based routes
|
||||
[Scene.Dashboards]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Dashboard]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Insights]: {
|
||||
projectBased: true,
|
||||
dark: true,
|
||||
},
|
||||
[Scene.Cohorts]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Events]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Sessions]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.SessionRecordings]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Person]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Persons]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Action]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.FeatureFlags]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.FeatureFlag]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Annotations]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Plugins]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.SavedInsights]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.ProjectSettings]: {
|
||||
projectBased: true,
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
[Scene.InsightRouter]: {
|
||||
projectBased: true,
|
||||
dark: true,
|
||||
},
|
||||
[Scene.Personalization]: {
|
||||
projectBased: true,
|
||||
plain: true,
|
||||
hideTopNav: true,
|
||||
},
|
||||
[Scene.Ingestion]: {
|
||||
projectBased: true,
|
||||
plain: true,
|
||||
},
|
||||
[Scene.OnboardingSetup]: {
|
||||
projectBased: true,
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
// Organization-based routes
|
||||
[Scene.OrganizationCreateFirst]: {
|
||||
plain: true,
|
||||
},
|
||||
[Scene.ProjectCreateFirst]: {
|
||||
plain: true,
|
||||
},
|
||||
[Scene.Billing]: {
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
// Onboarding/setup routes
|
||||
[Scene.Login]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.Signup]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.PreflightCheck]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.PasswordReset]: {
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
[Scene.PasswordResetComplete]: {
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
[Scene.InviteSignup]: {
|
||||
allowUnauthenticated: true,
|
||||
plain: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const redirects: Record<string, string | ((params: Params) => string)> = {
|
||||
'/': '/insights',
|
||||
'/dashboards': '/dashboard', // TODO: For consistency this should be the default, but we should make sure /dashboard keeps working
|
||||
'/plugins': '/project/plugins',
|
||||
'/actions': '/events/actions',
|
||||
'/organization/members': '/organization/settings',
|
||||
}
|
||||
|
||||
export const routes: Record<string, Scene> = {
|
||||
[urls.dashboards()]: Scene.Dashboards,
|
||||
[urls.dashboard(':id')]: Scene.Dashboard,
|
||||
[urls.createAction()]: Scene.Action,
|
||||
[urls.action(':id')]: Scene.Action,
|
||||
[urls.insights()]: Scene.Insights,
|
||||
[urls.insightRouter(':id')]: Scene.InsightRouter,
|
||||
[urls.events()]: Scene.Events,
|
||||
[urls.events() + '/*']: Scene.Events,
|
||||
[urls.sessions()]: Scene.Sessions,
|
||||
[urls.sessionRecordings()]: Scene.SessionRecordings,
|
||||
[urls.person('*')]: Scene.Person,
|
||||
[urls.persons()]: Scene.Persons,
|
||||
[urls.cohort(':id')]: Scene.Cohorts,
|
||||
[urls.cohorts()]: Scene.Cohorts,
|
||||
[urls.featureFlags()]: Scene.FeatureFlags,
|
||||
[urls.featureFlag(':id')]: Scene.FeatureFlag,
|
||||
[urls.annotations()]: Scene.Annotations,
|
||||
[urls.projectSettings()]: Scene.ProjectSettings,
|
||||
[urls.plugins()]: Scene.Plugins,
|
||||
[urls.projectCreateFirst()]: Scene.ProjectCreateFirst,
|
||||
[urls.organizationSettings()]: Scene.OrganizationSettings,
|
||||
[urls.organizationBilling()]: Scene.Billing,
|
||||
[urls.organizationCreateFirst()]: Scene.OrganizationCreateFirst,
|
||||
[urls.instanceLicenses()]: Scene.InstanceLicenses,
|
||||
[urls.systemStatus()]: Scene.SystemStatus,
|
||||
[urls.systemStatusPage(':id')]: Scene.SystemStatus,
|
||||
[urls.mySettings()]: Scene.MySettings,
|
||||
[urls.savedInsights()]: Scene.SavedInsights,
|
||||
// Onboarding / setup routes
|
||||
[urls.login()]: Scene.Login,
|
||||
[urls.preflight()]: Scene.PreflightCheck,
|
||||
[urls.signup()]: Scene.Signup,
|
||||
[urls.inviteSignup(':id')]: Scene.InviteSignup,
|
||||
[urls.passwordReset()]: Scene.PasswordReset,
|
||||
[urls.passwordResetComplete(':uuid', ':token')]: Scene.PasswordResetComplete,
|
||||
[urls.personalization()]: Scene.Personalization,
|
||||
[urls.ingestion()]: Scene.Ingestion,
|
||||
[urls.ingestion() + '/*']: Scene.Ingestion,
|
||||
[urls.onboardingSetup()]: Scene.OnboardingSetup,
|
||||
}
|
||||
|
||||
export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneConfig>>({
|
||||
export const sceneLogic = kea<sceneLogicType>({
|
||||
actions: {
|
||||
/* 1. Prepares to open the scene, as the listener may override and do something
|
||||
else (e.g. redirecting if unauthenticated), then calls (2) `loadScene`*/
|
||||
openScene: (scene: Scene, params: Params) => ({ scene, params }),
|
||||
openScene: (scene: Scene, params: SceneParams) => ({ scene, params }),
|
||||
// 2. Start loading the scene's Javascript and mount any logic, then calls (3) `setScene`
|
||||
loadScene: (scene: Scene, params: Params) => ({ scene, params }),
|
||||
loadScene: (scene: Scene, params: SceneParams) => ({ scene, params }),
|
||||
// 3. Set the `scene` reducer
|
||||
setScene: (scene: Scene, params: Params) => ({ scene, params }),
|
||||
setLoadedScene: (scene: Scene, loadedScene: LoadedScene) => ({ scene, loadedScene }),
|
||||
setScene: (scene: Scene, params: SceneParams) => ({ scene, params }),
|
||||
setLoadedScene: (loadedScene: LoadedScene) => ({
|
||||
loadedScene,
|
||||
}),
|
||||
showUpgradeModal: (featureName: string, featureCaption: string) => ({ featureName, featureCaption }),
|
||||
guardAvailableFeature: (
|
||||
featureKey: AvailableFeature,
|
||||
@ -322,16 +52,20 @@ export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneCo
|
||||
setScene: (_, payload) => payload.scene,
|
||||
},
|
||||
],
|
||||
params: [
|
||||
{} as Params,
|
||||
{
|
||||
setScene: (_, payload) => payload.params || {},
|
||||
},
|
||||
],
|
||||
loadedScenes: [
|
||||
preloadedScenes,
|
||||
{
|
||||
setLoadedScene: (state, { scene, loadedScene }) => ({ ...state, [scene]: loadedScene }),
|
||||
setScene: (state, { scene, params }) =>
|
||||
scene in state
|
||||
? {
|
||||
...state,
|
||||
[scene]: { ...state[scene], sceneParams: params, lastTouch: new Date().valueOf() },
|
||||
}
|
||||
: state,
|
||||
setLoadedScene: (state, { loadedScene }) => ({
|
||||
...state,
|
||||
[loadedScene.name]: { ...loadedScene, lastTouch: new Date().valueOf() },
|
||||
}),
|
||||
},
|
||||
],
|
||||
loadingScene: [
|
||||
@ -352,28 +86,40 @@ export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneCo
|
||||
},
|
||||
selectors: {
|
||||
sceneConfig: [
|
||||
(selectors) => [selectors.scene],
|
||||
(s) => [s.scene],
|
||||
(scene: Scene): SceneConfig => {
|
||||
return sceneConfigurations[scene] ?? {}
|
||||
},
|
||||
],
|
||||
activeScene: [
|
||||
(selectors) => [
|
||||
selectors.loadingScene,
|
||||
selectors.scene,
|
||||
(s) => [
|
||||
s.loadingScene,
|
||||
s.scene,
|
||||
teamLogic.selectors.isCurrentTeamUnavailable,
|
||||
featureFlagLogic.selectors.featureFlags,
|
||||
],
|
||||
(loadingScene, scene, isCurrentTeamUnavailable) => {
|
||||
const baseActiveScene = loadingScene || scene
|
||||
(loadingScene, scene, isCurrentTeamUnavailable, featureFlags) => {
|
||||
const baseActiveScene = featureFlags[FEATURE_FLAGS.TURBO_MODE] ? scene : loadingScene || scene
|
||||
return isCurrentTeamUnavailable && baseActiveScene && sceneConfigurations[baseActiveScene]?.projectBased
|
||||
? Scene.ErrorProjectUnavailable
|
||||
: baseActiveScene
|
||||
},
|
||||
],
|
||||
activeLoadedScene: [
|
||||
(s) => [s.activeScene, s.loadedScenes],
|
||||
(activeScene, loadedScenes) => (activeScene ? loadedScenes[activeScene] : null),
|
||||
],
|
||||
sceneParams: [
|
||||
(s) => [s.activeLoadedScene],
|
||||
(activeLoadedScene): SceneParams =>
|
||||
activeLoadedScene?.sceneParams || { params: {}, searchParams: {}, hashParams: {} },
|
||||
],
|
||||
params: [(s) => [s.sceneParams], (sceneParams): Record<string, string> => sceneParams.params || {}],
|
||||
searchParams: [(s) => [s.sceneParams], (sceneParams): Record<string, any> => sceneParams.searchParams || {}],
|
||||
hashParams: [(s) => [s.sceneParams], (sceneParams): Record<string, any> => sceneParams.hashParams || {}],
|
||||
},
|
||||
urlToAction: ({ actions }) => {
|
||||
const mapping: Record<string, (params: Params) => any> = {}
|
||||
const mapping: Record<string, (params: Params, searchParams: Params, hashParams: Params) => any> = {}
|
||||
|
||||
for (const path of Object.keys(redirects)) {
|
||||
mapping[path] = (params) => {
|
||||
@ -382,10 +128,11 @@ export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneCo
|
||||
}
|
||||
}
|
||||
for (const [path, scene] of Object.entries(routes)) {
|
||||
mapping[path] = (params) => actions.openScene(scene, params)
|
||||
mapping[path] = (params, searchParams, hashParams) =>
|
||||
actions.openScene(scene, { params, searchParams, hashParams })
|
||||
}
|
||||
|
||||
mapping['/*'] = () => actions.loadScene(Scene.Error404, {})
|
||||
mapping['/*'] = () => actions.loadScene(Scene.Error404, emptySceneParams)
|
||||
|
||||
return mapping
|
||||
},
|
||||
@ -472,23 +219,14 @@ export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneCo
|
||||
|
||||
actions.loadScene(scene, params)
|
||||
},
|
||||
loadScene: async (
|
||||
{
|
||||
scene,
|
||||
params = {},
|
||||
}: {
|
||||
scene: Scene
|
||||
params: Params
|
||||
},
|
||||
breakpoint
|
||||
) => {
|
||||
loadScene: async ({ scene, params }, breakpoint) => {
|
||||
if (values.scene === scene) {
|
||||
actions.setScene(scene, params)
|
||||
return
|
||||
}
|
||||
|
||||
if (!scenes[scene]) {
|
||||
actions.setScene(Scene.Error404, {})
|
||||
actions.setScene(Scene.Error404, emptySceneParams)
|
||||
return
|
||||
}
|
||||
|
||||
@ -508,58 +246,56 @@ export const sceneLogic = kea<sceneLogicType<LoadedScene, Params, Scene, SceneCo
|
||||
} else {
|
||||
// First scene, show an error page
|
||||
console.error('App assets regenerated. Showing error page.')
|
||||
actions.setScene(Scene.ErrorNetwork, {})
|
||||
actions.setScene(Scene.ErrorNetwork, emptySceneParams)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
breakpoint()
|
||||
const { default: defaultExport, logic, ...others } = importedScene
|
||||
const { default: defaultExport, logic, scene: _scene, ...others } = importedScene
|
||||
|
||||
if (defaultExport) {
|
||||
if (_scene) {
|
||||
loadedScene = { name: scene, ...(_scene as SceneExport), sceneParams: params }
|
||||
} else if (defaultExport) {
|
||||
console.warn(`Scene ${scene} not yet converted to use SceneExport!`)
|
||||
loadedScene = {
|
||||
name: scene,
|
||||
component: defaultExport,
|
||||
logic: logic,
|
||||
sceneParams: params,
|
||||
}
|
||||
} else {
|
||||
console.warn(`Scene ${scene} not yet converted to use SceneExport!`)
|
||||
loadedScene = {
|
||||
name: scene,
|
||||
component:
|
||||
Object.keys(others).length === 1
|
||||
? others[Object.keys(others)[0]]
|
||||
: values.loadedScenes[Scene.Error404].component,
|
||||
logic: logic,
|
||||
sceneParams: params,
|
||||
}
|
||||
if (Object.keys(others).length > 1) {
|
||||
console.error('There are multiple exports for this scene. Showing 404 instead.')
|
||||
}
|
||||
}
|
||||
actions.setLoadedScene(scene, loadedScene)
|
||||
}
|
||||
const { logic } = loadedScene
|
||||
actions.setLoadedScene(loadedScene)
|
||||
|
||||
let unmount
|
||||
|
||||
if (logic) {
|
||||
// initialize the logic
|
||||
unmount = logic.build(params, false).mount()
|
||||
try {
|
||||
await breakpoint(100)
|
||||
} catch (e) {
|
||||
// if we change the scene while waiting these 100ms, unmount
|
||||
unmount()
|
||||
throw e
|
||||
let unmount
|
||||
if (featureFlagLogic.values.featureFlags[FEATURE_FLAGS.TURBO_MODE] && loadedScene.logic) {
|
||||
// initialize the logic and give it 50ms to load before opening the scene
|
||||
unmount = loadedScene.logic.build(loadedScene.paramsToProps?.(params) || {}, false).mount()
|
||||
try {
|
||||
await breakpoint(50)
|
||||
} catch (e) {
|
||||
// if we change the scene while waiting these 50ms, unmount
|
||||
unmount()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions.setScene(scene, params)
|
||||
|
||||
if (unmount) {
|
||||
// release our hold on this logic after 0.5s as it's by then surely mounted via React
|
||||
// or we are anyway in a new scene and don't need it
|
||||
await delay(500)
|
||||
unmount()
|
||||
}
|
||||
},
|
||||
setPageTitle: ({ title }) => {
|
||||
document.title = title ? `${title} • PostHog` : 'PostHog'
|
||||
|
91
frontend/src/scenes/sceneTypes.ts
Normal file
91
frontend/src/scenes/sceneTypes.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { LogicWrapper } from 'kea'
|
||||
|
||||
export enum Scene {
|
||||
Error404 = '404',
|
||||
ErrorNetwork = '4xx',
|
||||
ErrorProjectUnavailable = 'projectUnavailable',
|
||||
Dashboards = 'dashboards',
|
||||
Dashboard = 'dashboard',
|
||||
Insights = 'insights',
|
||||
InsightRouter = 'insightRouter',
|
||||
Cohorts = 'cohorts',
|
||||
Events = 'events',
|
||||
EventStats = 'eventStats',
|
||||
EventPropertyStats = 'eventPropertyStats',
|
||||
Sessions = 'sessions',
|
||||
SessionRecordings = 'sessionRecordings',
|
||||
Person = 'person',
|
||||
Persons = 'persons',
|
||||
Action = 'action',
|
||||
Actions = 'actions',
|
||||
FeatureFlags = 'featureFlags',
|
||||
FeatureFlag = 'featureFlag',
|
||||
OrganizationSettings = 'organizationSettings',
|
||||
OrganizationCreateFirst = 'organizationCreateFirst',
|
||||
ProjectSettings = 'projectSettings',
|
||||
ProjectCreateFirst = 'projectCreateFirst',
|
||||
SystemStatus = 'systemStatus',
|
||||
InstanceLicenses = 'instanceLicenses',
|
||||
MySettings = 'mySettings',
|
||||
Annotations = 'annotations',
|
||||
Billing = 'billing',
|
||||
Plugins = 'plugins',
|
||||
SavedInsights = 'savedInsights',
|
||||
// Authentication & onboarding routes
|
||||
Login = 'login',
|
||||
Signup = 'signup',
|
||||
InviteSignup = 'inviteSignup',
|
||||
PasswordReset = 'passwordReset',
|
||||
PasswordResetComplete = 'passwordResetComplete',
|
||||
PreflightCheck = 'preflightCheck',
|
||||
Ingestion = 'ingestion',
|
||||
OnboardingSetup = 'onboardingSetup',
|
||||
Personalization = 'personalization',
|
||||
}
|
||||
|
||||
export type SceneProps = Record<string, any>
|
||||
|
||||
export type SceneComponent = (params?: SceneProps) => JSX.Element | null
|
||||
|
||||
export interface SceneExport {
|
||||
/** component to render for this scene */
|
||||
component: SceneComponent
|
||||
/** logic to mount for this scene */
|
||||
logic?: LogicWrapper
|
||||
/** convert URL parameters from scenes.ts into logic props */
|
||||
paramsToProps?: (params: SceneParams) => SceneProps
|
||||
/** when was the scene last touched, unix timestamp for sortability */
|
||||
lastTouch?: number
|
||||
}
|
||||
|
||||
export interface LoadedScene extends SceneExport {
|
||||
name: string
|
||||
sceneParams: SceneParams
|
||||
}
|
||||
|
||||
export interface SceneParams {
|
||||
params: Record<string, any>
|
||||
searchParams: Record<string, any>
|
||||
hashParams: Record<string, any>
|
||||
}
|
||||
|
||||
export interface Params {
|
||||
[param: string]: any
|
||||
}
|
||||
|
||||
export interface SceneConfig {
|
||||
/** Route should only be accessed when logged out (N.B. should be added to posthog/urls.py too) */
|
||||
onlyUnauthenticated?: boolean
|
||||
/** Route **can** be accessed when logged out (i.e. can be accessed when logged in too; should be added to posthog/urls.py too) */
|
||||
allowUnauthenticated?: boolean
|
||||
/** Background is $bg_mid */
|
||||
dark?: boolean
|
||||
/** Only keeps the main content and the top navigation bar */
|
||||
plain?: boolean
|
||||
/** Hides the top navigation bar (regardless of whether `plain` is `true` or not) */
|
||||
hideTopNav?: boolean
|
||||
/** Hides demo project warnings (DemoWarning.tsx) */
|
||||
hideDemoWarnings?: boolean
|
||||
/** Route requires project access */
|
||||
projectBased?: boolean
|
||||
}
|
226
frontend/src/scenes/scenes.ts
Normal file
226
frontend/src/scenes/scenes.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import { Params, Scene, SceneConfig, LoadedScene } from 'scenes/sceneTypes'
|
||||
import { Error404 as Error404Component } from '~/layout/Error404'
|
||||
import { ErrorNetwork as ErrorNetworkComponent } from '~/layout/ErrorNetwork'
|
||||
import { ErrorProjectUnavailable as ErrorProjectUnavailableComponent } from '~/layout/ErrorProjectUnavailable'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
export const emptySceneParams = { params: {}, searchParams: {}, hashParams: {} }
|
||||
|
||||
export const preloadedScenes: Record<string, LoadedScene> = {
|
||||
[Scene.Error404]: {
|
||||
name: Scene.Error404,
|
||||
component: Error404Component,
|
||||
sceneParams: emptySceneParams,
|
||||
},
|
||||
[Scene.ErrorNetwork]: {
|
||||
name: Scene.ErrorNetwork,
|
||||
component: ErrorNetworkComponent,
|
||||
sceneParams: emptySceneParams,
|
||||
},
|
||||
[Scene.ErrorProjectUnavailable]: {
|
||||
name: Scene.ErrorProjectUnavailable,
|
||||
component: ErrorProjectUnavailableComponent,
|
||||
sceneParams: emptySceneParams,
|
||||
},
|
||||
}
|
||||
|
||||
export const scenes: Record<Scene, () => any> = {
|
||||
[Scene.Error404]: () => ({ default: preloadedScenes[Scene.Error404].component }),
|
||||
[Scene.ErrorNetwork]: () => ({ default: preloadedScenes[Scene.ErrorNetwork].component }),
|
||||
[Scene.ErrorProjectUnavailable]: () => ({ default: preloadedScenes[Scene.ErrorProjectUnavailable].component }),
|
||||
[Scene.Dashboards]: () => import(/* webpackChunkName: 'dashboards' */ './dashboard/Dashboards'),
|
||||
[Scene.Dashboard]: () => import(/* webpackChunkName: 'dashboard' */ './dashboard/Dashboard'),
|
||||
[Scene.Insights]: () => import(/* webpackChunkName: 'insights' */ './insights/Insights'),
|
||||
[Scene.InsightRouter]: () => import(/* webpackChunkName: 'insightRouter' */ './insights/InsightRouter'),
|
||||
[Scene.Cohorts]: () => import(/* webpackChunkName: 'cohorts' */ './cohorts/Cohorts'),
|
||||
|
||||
[Scene.Events]: () => import(/* webpackChunkName: 'events' */ './events/EventsTable'),
|
||||
[Scene.Actions]: () => import(/* webpackChunkName: 'events' */ './actions/ActionsTable'),
|
||||
[Scene.EventStats]: () => import(/* webpackChunkName: 'events' */ './events/EventsVolumeTable'),
|
||||
[Scene.EventPropertyStats]: () => import(/* webpackChunkName: 'events' */ './events/PropertiesVolumeTable'),
|
||||
|
||||
[Scene.Sessions]: () => import(/* webpackChunkName: 'sessions' */ './sessions/Sessions'),
|
||||
[Scene.SessionRecordings]: () =>
|
||||
import(/* webpackChunkName: 'sessionRecordings' */ './session-recordings/SessionRecordings'),
|
||||
[Scene.Person]: () => import(/* webpackChunkName: 'person' */ './persons/Person'),
|
||||
[Scene.Persons]: () => import(/* webpackChunkName: 'persons' */ './persons/Persons'),
|
||||
[Scene.Action]: () => import(/* webpackChunkName: 'action' */ './actions/Action'), // TODO
|
||||
[Scene.FeatureFlags]: () => import(/* webpackChunkName: 'featureFlags' */ './experimentation/FeatureFlags'),
|
||||
[Scene.FeatureFlag]: () => import(/* webpackChunkName: 'featureFlag' */ './experimentation/FeatureFlag'),
|
||||
[Scene.OrganizationSettings]: () =>
|
||||
import(/* webpackChunkName: 'organizationSettings' */ './organization/Settings'),
|
||||
[Scene.OrganizationCreateFirst]: () =>
|
||||
import(/* webpackChunkName: 'organizationCreateFirst' */ './organization/Create'),
|
||||
[Scene.ProjectSettings]: () => import(/* webpackChunkName: 'projectSettings' */ './project/Settings'),
|
||||
[Scene.ProjectCreateFirst]: () => import(/* webpackChunkName: 'projectCreateFirst' */ './project/Create'),
|
||||
[Scene.SystemStatus]: () => import(/* webpackChunkName: 'systemStatus' */ './instance/SystemStatus'),
|
||||
[Scene.InstanceLicenses]: () => import(/* webpackChunkName: 'instanceLicenses' */ './instance/Licenses'),
|
||||
[Scene.MySettings]: () => import(/* webpackChunkName: 'mySettings' */ './me/Settings'),
|
||||
[Scene.Annotations]: () => import(/* webpackChunkName: 'annotations' */ './annotations'),
|
||||
[Scene.PreflightCheck]: () => import(/* webpackChunkName: 'preflightCheck' */ './PreflightCheck'),
|
||||
[Scene.Signup]: () => import(/* webpackChunkName: 'signup' */ './authentication/Signup'),
|
||||
[Scene.InviteSignup]: () => import(/* webpackChunkName: 'inviteSignup' */ './authentication/InviteSignup'),
|
||||
[Scene.Ingestion]: () => import(/* webpackChunkName: 'ingestion' */ './ingestion/IngestionWizard'),
|
||||
[Scene.Billing]: () => import(/* webpackChunkName: 'billing' */ './billing/Billing'),
|
||||
[Scene.Plugins]: () => import(/* webpackChunkName: 'plugins' */ './plugins/Plugins'),
|
||||
[Scene.Personalization]: () => import(/* webpackChunkName: 'personalization' */ './onboarding/Personalization'),
|
||||
[Scene.OnboardingSetup]: () => import(/* webpackChunkName: 'onboardingSetup' */ './onboarding/OnboardingSetup'),
|
||||
[Scene.Login]: () => import(/* webpackChunkName: 'login' */ './authentication/Login'),
|
||||
[Scene.SavedInsights]: () => import(/* webpackChunkName: 'savedInsights' */ './saved-insights/SavedInsights'),
|
||||
[Scene.PasswordReset]: () => import(/* webpackChunkName: 'passwordReset' */ './authentication/PasswordReset'),
|
||||
[Scene.PasswordResetComplete]: () =>
|
||||
import(/* webpackChunkName: 'passwordResetComplete' */ './authentication/PasswordResetComplete'),
|
||||
}
|
||||
|
||||
export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
|
||||
// Project-based routes
|
||||
[Scene.Dashboards]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Dashboard]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Insights]: {
|
||||
projectBased: true,
|
||||
dark: true,
|
||||
},
|
||||
[Scene.Cohorts]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Events]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Sessions]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.SessionRecordings]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Person]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Persons]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Action]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.FeatureFlags]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.FeatureFlag]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Annotations]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.Plugins]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.SavedInsights]: {
|
||||
projectBased: true,
|
||||
},
|
||||
[Scene.ProjectSettings]: {
|
||||
projectBased: true,
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
[Scene.InsightRouter]: {
|
||||
projectBased: true,
|
||||
dark: true,
|
||||
},
|
||||
[Scene.Personalization]: {
|
||||
projectBased: true,
|
||||
plain: true,
|
||||
hideTopNav: true,
|
||||
},
|
||||
[Scene.Ingestion]: {
|
||||
projectBased: true,
|
||||
plain: true,
|
||||
},
|
||||
[Scene.OnboardingSetup]: {
|
||||
projectBased: true,
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
// Organization-based routes
|
||||
[Scene.OrganizationCreateFirst]: {
|
||||
plain: true,
|
||||
},
|
||||
[Scene.ProjectCreateFirst]: {
|
||||
plain: true,
|
||||
},
|
||||
[Scene.Billing]: {
|
||||
hideDemoWarnings: true,
|
||||
},
|
||||
// Onboarding/setup routes
|
||||
[Scene.Login]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.Signup]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.PreflightCheck]: {
|
||||
onlyUnauthenticated: true,
|
||||
},
|
||||
[Scene.PasswordReset]: {
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
[Scene.PasswordResetComplete]: {
|
||||
allowUnauthenticated: true,
|
||||
},
|
||||
[Scene.InviteSignup]: {
|
||||
allowUnauthenticated: true,
|
||||
plain: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const redirects: Record<string, string | ((params: Params) => string)> = {
|
||||
'/': urls.insights(),
|
||||
'/dashboards': urls.dashboards(),
|
||||
'/plugins': urls.plugins(),
|
||||
'/actions': '/events/actions',
|
||||
'/organization/members': urls.organizationSettings(),
|
||||
}
|
||||
|
||||
export const routes: Record<string, Scene> = {
|
||||
[urls.dashboards()]: Scene.Dashboards,
|
||||
[urls.dashboard(':id')]: Scene.Dashboard,
|
||||
[urls.createAction()]: Scene.Action,
|
||||
[urls.action(':id')]: Scene.Action,
|
||||
[urls.insights()]: Scene.Insights,
|
||||
[urls.insightRouter(':id')]: Scene.InsightRouter,
|
||||
[urls.actions()]: Scene.Actions,
|
||||
[urls.eventStats()]: Scene.EventStats,
|
||||
[urls.eventPropertyStats()]: Scene.EventPropertyStats,
|
||||
[urls.events()]: Scene.Events,
|
||||
[urls.sessions()]: Scene.Sessions,
|
||||
[urls.sessionRecordings()]: Scene.SessionRecordings,
|
||||
[urls.person('*')]: Scene.Person,
|
||||
[urls.persons()]: Scene.Persons,
|
||||
[urls.cohort(':id')]: Scene.Cohorts,
|
||||
[urls.cohorts()]: Scene.Cohorts,
|
||||
[urls.featureFlags()]: Scene.FeatureFlags,
|
||||
[urls.featureFlag(':id')]: Scene.FeatureFlag,
|
||||
[urls.annotations()]: Scene.Annotations,
|
||||
[urls.projectSettings()]: Scene.ProjectSettings,
|
||||
[urls.plugins()]: Scene.Plugins,
|
||||
[urls.projectCreateFirst()]: Scene.ProjectCreateFirst,
|
||||
[urls.organizationSettings()]: Scene.OrganizationSettings,
|
||||
[urls.organizationBilling()]: Scene.Billing,
|
||||
[urls.organizationCreateFirst()]: Scene.OrganizationCreateFirst,
|
||||
[urls.instanceLicenses()]: Scene.InstanceLicenses,
|
||||
[urls.systemStatus()]: Scene.SystemStatus,
|
||||
[urls.systemStatusPage(':id')]: Scene.SystemStatus,
|
||||
[urls.mySettings()]: Scene.MySettings,
|
||||
[urls.savedInsights()]: Scene.SavedInsights,
|
||||
// Onboarding / setup routes
|
||||
[urls.login()]: Scene.Login,
|
||||
[urls.preflight()]: Scene.PreflightCheck,
|
||||
[urls.signup()]: Scene.Signup,
|
||||
[urls.inviteSignup(':id')]: Scene.InviteSignup,
|
||||
[urls.passwordReset()]: Scene.PasswordReset,
|
||||
[urls.passwordResetComplete(':uuid', ':token')]: Scene.PasswordResetComplete,
|
||||
[urls.personalization()]: Scene.Personalization,
|
||||
[urls.ingestion()]: Scene.Ingestion,
|
||||
[urls.ingestion() + '/*']: Scene.Ingestion,
|
||||
[urls.onboardingSetup()]: Scene.OnboardingSetup,
|
||||
}
|
@ -7,6 +7,8 @@ import { useValues } from 'kea'
|
||||
import { router } from 'kea-router'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { ArrowRightOutlined } from '@ant-design/icons'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { sessionRecordingsTableLogic } from 'scenes/session-recordings/sessionRecordingsTableLogic'
|
||||
|
||||
export function SessionsRecordings(): JSX.Element {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
@ -42,3 +44,8 @@ export function SessionsRecordings(): JSX.Element {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: SessionsRecordings,
|
||||
logic: sessionRecordingsTableLogic,
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ export const sessionsPlayLogic = kea<sessionsPlayLogicType>({
|
||||
color: 'orange',
|
||||
}))
|
||||
|
||||
return pageChangeEvents.concat(highlightedEvents).sort((a, b) => a.playerTime - b.playerTime)
|
||||
return [...pageChangeEvents, ...highlightedEvents].sort((a, b) => a.playerTime - b.playerTime)
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -24,7 +24,7 @@ export function ActionsPie({
|
||||
const { results } = useValues(logic)
|
||||
|
||||
function updateData(): void {
|
||||
const _data = results as TrendResultWithAggregate[]
|
||||
const _data = [...results] as TrendResultWithAggregate[]
|
||||
_data.sort((a, b) => b.aggregated_value - a.aggregated_value)
|
||||
const days = results.length > 0 ? results[0].days : []
|
||||
|
||||
|
@ -14,7 +14,7 @@ export function ActionsTable(): JSX.Element {
|
||||
|
||||
let data = indexedResults as any as TrendResultWithAggregate[]
|
||||
if (!filters.session && data) {
|
||||
data = data.sort((a, b) => b.aggregated_value - a.aggregated_value)
|
||||
data = [...data].sort((a, b) => b.aggregated_value - a.aggregated_value)
|
||||
}
|
||||
|
||||
data = data.map((d: any) => {
|
||||
|
@ -8,12 +8,14 @@ export const urls = {
|
||||
dashboard: (id: string | number) => `/dashboard/${id}`,
|
||||
createAction: () => `/action`, // TODO: For consistency, this should be `/action/new`
|
||||
action: (id: string | number) => `/action/${id}`,
|
||||
actions: () => '/actions',
|
||||
actions: () => '/events/actions',
|
||||
eventStats: () => '/events/stats',
|
||||
eventPropertyStats: () => '/events/properties',
|
||||
events: () => '/events',
|
||||
insights: () => '/insights',
|
||||
insightView: (view: ViewType) => `/insights?insight=${view}`,
|
||||
insightRouter: (id: string) => `/i/${id}`,
|
||||
savedInsights: () => '/saved_insights',
|
||||
events: () => '/events',
|
||||
sessions: () => '/sessions',
|
||||
sessionRecordings: () => '/recordings',
|
||||
person: (id: string) => `/person/${id}`,
|
||||
|
@ -4,8 +4,32 @@ import { testUtilsPlugin, expectLogic } from 'kea-test-utils'
|
||||
import { createMemoryHistory } from 'history'
|
||||
import posthog from 'posthog-js'
|
||||
import { AppContext } from '../types'
|
||||
import { MOCK_TEAM_ID } from '../lib/api.mock'
|
||||
import { MOCK_DEFAULT_TEAM } from '../lib/api.mock'
|
||||
|
||||
export function initKeaTests(): void {
|
||||
window.POSTHOG_APP_CONTEXT = {
|
||||
current_team: MOCK_DEFAULT_TEAM,
|
||||
...window.POSTHOG_APP_CONTEXT,
|
||||
} as unknown as AppContext
|
||||
posthog.init('no token', {
|
||||
api_host: 'borked',
|
||||
test: true,
|
||||
autocapture: false,
|
||||
disable_session_recording: true,
|
||||
advanced_disable_decide: true,
|
||||
opt_out_capturing_by_default: true,
|
||||
loaded: (p) => {
|
||||
p.opt_out_capturing()
|
||||
},
|
||||
})
|
||||
|
||||
const history = createMemoryHistory()
|
||||
;(history as any).pushState = history.push
|
||||
;(history as any).replaceState = history.replace
|
||||
initKea({ beforePlugins: [testUtilsPlugin], routerLocation: history.location, routerHistory: history })
|
||||
}
|
||||
|
||||
/* do not call this within a 'test' or a 'beforeEach' block, only in 'describe' */
|
||||
export function initKeaTestLogic<L extends Logic = Logic>({
|
||||
logic,
|
||||
props,
|
||||
@ -19,36 +43,18 @@ export function initKeaTestLogic<L extends Logic = Logic>({
|
||||
let unmount: () => void
|
||||
|
||||
beforeEach(async () => {
|
||||
window.POSTHOG_APP_CONTEXT = {
|
||||
current_team: { id: MOCK_TEAM_ID },
|
||||
...window.POSTHOG_APP_CONTEXT,
|
||||
} as unknown as AppContext
|
||||
posthog.init('no token', {
|
||||
api_host: 'borked',
|
||||
test: true,
|
||||
autocapture: false,
|
||||
disable_session_recording: true,
|
||||
advanced_disable_decide: true,
|
||||
opt_out_capturing_by_default: true,
|
||||
loaded: (p) => {
|
||||
p.opt_out_capturing()
|
||||
},
|
||||
})
|
||||
|
||||
const history = createMemoryHistory()
|
||||
;(history as any).pushState = history.push
|
||||
;(history as any).replaceState = history.replace
|
||||
initKea({ beforePlugins: [testUtilsPlugin], routerLocation: history.location, routerHistory: history })
|
||||
initKeaTests()
|
||||
if (logic) {
|
||||
builtLogic = logic.build({ ...props })
|
||||
await onLogic?.(builtLogic)
|
||||
unmount = builtLogic.mount()
|
||||
}
|
||||
return unmount
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (logic) {
|
||||
unmount()
|
||||
unmount?.()
|
||||
await expectLogic(logic).toFinishAllListeners()
|
||||
}
|
||||
delete window.POSTHOG_APP_CONTEXT
|
||||
|
@ -1039,6 +1039,12 @@ export interface InsightLogicProps {
|
||||
doNotLoad?: boolean
|
||||
}
|
||||
|
||||
export interface SetInsightOptions {
|
||||
shouldMergeWithExisting?: boolean
|
||||
/** this overrides the in-flight filters on the page, which may not equal the last returned API response */
|
||||
overrideFilter?: boolean
|
||||
}
|
||||
|
||||
export interface FeatureFlagGroupType {
|
||||
properties: AnyPropertyFilter[]
|
||||
rollout_percentage: number | null
|
||||
|
@ -78,10 +78,10 @@
|
||||
"expr-eval": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^6.4.1",
|
||||
"kea": "^2.5.1",
|
||||
"kea": "^2.5.4",
|
||||
"kea-loaders": "^0.5.1",
|
||||
"kea-localstorage": "^1.1.1",
|
||||
"kea-router": "^1.0.2",
|
||||
"kea-router": "^1.0.5",
|
||||
"kea-waitfor": "^0.2.1",
|
||||
"kea-window-values": "^0.1.0",
|
||||
"md5": "^2.3.0",
|
||||
|
16
yarn.lock
16
yarn.lock
@ -10719,10 +10719,10 @@ kea-localstorage@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/kea-localstorage/-/kea-localstorage-1.1.1.tgz#6edf69476779e002708fb10f2360e48333707406"
|
||||
integrity sha512-YijSF33Y1QpfHAq1hGvulWWoGC9Kckd4lVa+XStHar4t7GfoaDa1aWnTeJxzz9bWnJovfk+MnG8ekP4rWIqINA==
|
||||
|
||||
kea-router@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kea-router/-/kea-router-1.0.2.tgz#7ba20e6e5c97ae353918e38c0ca7a3eb9ed44e00"
|
||||
integrity sha512-PsQhfl0j3kRcggBwC02+YsKzFa8zk46BSj9DbZvy/JzVXlBO7L3BTryo6kQSpZbU6v0HJgr7Oupaj396zsr9bA==
|
||||
kea-router@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/kea-router/-/kea-router-1.0.5.tgz#1e6e2e820bf012904589b356356dcd3156360fb9"
|
||||
integrity sha512-EIY/0SmdtLrKTxmoE+MaB1GOQ0HHr4a38TS4ub4ylQ7mQ+lyC2D8m4KIbYRYJ9wFvkEK2QOMsuVQbmfD1AXEOA==
|
||||
dependencies:
|
||||
url-pattern "^1.0.3"
|
||||
|
||||
@ -10753,10 +10753,10 @@ kea-window-values@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/kea-window-values/-/kea-window-values-0.1.0.tgz#d6e2045c14521082d9fd88f4e4a5e2c596ac4d0e"
|
||||
integrity sha512-4OsoHaVU4+KFyfL4ZWKB5lVQbx6A7Azc2SVraGrzIglS/3DN/97WZ6RrDMHuitu02o9cm/CRmMsNB8FXSD5xdQ==
|
||||
|
||||
kea@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/kea/-/kea-2.5.1.tgz#7b396e11acd1aad65d2cdee0e19d3755c3d7830e"
|
||||
integrity sha512-yvhnM/QHd70bCZwablIKDXuUoqrNljYoc5H4ibrUc0MW/ZrN/KTvesjXGAgkGb01m2H8jXFnzGOQXkxxngVtoQ==
|
||||
kea@^2.5.4:
|
||||
version "2.5.4"
|
||||
resolved "https://registry.yarnpkg.com/kea/-/kea-2.5.4.tgz#0c4cdcab9a4c62b59377332fcc1637959f47cf3f"
|
||||
integrity sha512-MFWPzkngDeKYQepGQtcujvXZYzlpMnHUYQAnMl3OJOzOkHHK81KFNY4T80x+l9ePb9Akn5Jmat7fbWcnS7/XBA==
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user