feat: Sidebar cohorts and annotations movement (#18200)
@@ -16,9 +16,7 @@ describe('a11y', () => {
|
||||
'experiments',
|
||||
'events',
|
||||
'datamanagement',
|
||||
'persons',
|
||||
'cohorts',
|
||||
'annotations',
|
||||
'personsmanagement',
|
||||
'apps',
|
||||
'toolbarlaunch',
|
||||
'projectsettings',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
describe('Annotations', () => {
|
||||
beforeEach(() => {
|
||||
cy.clickNavMenu('annotations')
|
||||
cy.clickNavMenu('datamanagement')
|
||||
cy.get('[data-attr=data-management-annotations-tab]').click()
|
||||
})
|
||||
|
||||
it('Annotations loaded', () => {
|
||||
cy.get('h1').should('contain', 'Annotations')
|
||||
cy.get('h2').should('contain', 'Create your first annotation')
|
||||
cy.get('[data-attr="product-introduction-docs-link"]').should('contain', 'Learn more about Annotations')
|
||||
})
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
describe('Cohorts', () => {
|
||||
const goToCohorts = (): void => {
|
||||
cy.clickNavMenu('personsmanagement')
|
||||
cy.get('[data-attr=persons-management-cohorts-tab]').click()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clickNavMenu('cohorts')
|
||||
goToCohorts()
|
||||
})
|
||||
|
||||
it('Cohorts new and list', () => {
|
||||
// load an empty page
|
||||
cy.get('h1').should('contain', 'Cohorts')
|
||||
cy.title().should('equal', 'Cohorts • PostHog')
|
||||
cy.get('h1').should('contain', 'People')
|
||||
cy.title().should('equal', 'Cohorts • People • PostHog')
|
||||
cy.get('h2').should('contain', 'Create your first cohort')
|
||||
cy.get('[data-attr="product-introduction-docs-link"]').should('contain', 'Learn more about Cohorts')
|
||||
|
||||
// go to create a new cohort
|
||||
cy.get('[data-attr="create-cohort"]').click()
|
||||
cy.get('[data-attr="new-cohort"]').click()
|
||||
|
||||
// select "add filter" and "property"
|
||||
cy.get('[data-attr="cohort-selector-field-value"]').click()
|
||||
@@ -34,7 +39,7 @@ describe('Cohorts', () => {
|
||||
cy.get('[data-attr=success-toast]').contains('Cohort saved').should('exist')
|
||||
|
||||
// back to cohorts
|
||||
cy.clickNavMenu('cohorts')
|
||||
goToCohorts()
|
||||
cy.get('tbody').contains('Test Cohort')
|
||||
cy.get('h2').should('not.have.text', 'Create your first cohort')
|
||||
|
||||
@@ -69,7 +74,7 @@ describe('Cohorts', () => {
|
||||
// delete cohort
|
||||
cy.get('[data-attr="more-button"]').click()
|
||||
cy.get('.Popover__content').contains('Delete cohort').click()
|
||||
cy.clickNavMenu('cohorts')
|
||||
goToCohorts()
|
||||
cy.get('tbody').should('not.have.text', 'Test Cohort (dynamic copy) (static copy)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,11 +38,9 @@ describe('Insights', () => {
|
||||
const insightName = randomString('to save and then navigate away from')
|
||||
insight.create(insightName)
|
||||
|
||||
cy.get('[data-attr="menu-item-annotations"]').click()
|
||||
cy.get('[data-attr="menu-item-dashboards"]').click()
|
||||
|
||||
// the annotations API call is made before the annotations page loads, so we can't wait for it
|
||||
cy.get('[data-attr="annotations-content"]').should('exist')
|
||||
cy.url().should('include', '/annotations')
|
||||
cy.url().should('include', '/dashboard')
|
||||
})
|
||||
|
||||
it('Can keep editing changed new insight after navigating away with confirm() rejection (case 1)', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
describe('Person Visualization Check', () => {
|
||||
beforeEach(() => {
|
||||
cy.clickNavMenu('persons')
|
||||
cy.clickNavMenu('personsmanagement')
|
||||
cy.location('pathname').should('eq', '/persons')
|
||||
cy.get('.ant-spin-spinning').should('not.exist') // Wait until initial table load to be able to use the search
|
||||
cy.wait(1000)
|
||||
cy.get('[data-attr=persons-search]').type('deb').should('have.value', 'deb')
|
||||
cy.contains('deborah.fernandez@gmail.com').should('not.exist')
|
||||
cy.contains('deborah.fernandez@gmail.com').click()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
describe('Persons', () => {
|
||||
beforeEach(() => {
|
||||
cy.clickNavMenu('persons')
|
||||
cy.clickNavMenu('personsmanagement')
|
||||
})
|
||||
|
||||
it('All tabs work', () => {
|
||||
cy.get('h1').should('contain', 'Persons')
|
||||
cy.get('h1').should('contain', 'People')
|
||||
cy.get('[data-attr=persons-search]').type('marisol').type('{enter}').should('have.value', 'marisol')
|
||||
cy.wait(200)
|
||||
cy.get('[data-row-key]').its('length').should('be.gte', 0)
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
@@ -4,6 +4,7 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import clsx from 'clsx'
|
||||
import { useValues } from 'kea'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { SidebarChangeNoticeContent, useSidebarChangeNotices } from '~/layout/navigation/SideBar/SidebarChangeNotice'
|
||||
import { navigation3000Logic } from '../navigationLogic'
|
||||
import { LemonTag } from '@posthog/lemon-ui'
|
||||
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
|
||||
@@ -63,29 +64,46 @@ export const NavbarButton: FunctionComponent<NavbarButtonProps> = React.forwardR
|
||||
}
|
||||
}
|
||||
|
||||
const buttonContent = (
|
||||
<LemonButton
|
||||
ref={ref}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onMouseEnter={() => setHasBeenClicked(false)}
|
||||
onClick={() => {
|
||||
setHasBeenClicked(true)
|
||||
onClick?.()
|
||||
}}
|
||||
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
|
||||
fullWidth
|
||||
{...buttonProps}
|
||||
>
|
||||
{content}
|
||||
</LemonButton>
|
||||
)
|
||||
|
||||
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
|
||||
|
||||
return (
|
||||
<li className="w-full">
|
||||
<Tooltip
|
||||
title={isNavCollapsedActually ? (here ? `${title} (you are here)` : title) : null}
|
||||
placement="right"
|
||||
delayMs={0}
|
||||
visible={!persistentTooltip && hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
|
||||
>
|
||||
<LemonButton
|
||||
ref={ref}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onMouseEnter={() => setHasBeenClicked(false)}
|
||||
onClick={() => {
|
||||
setHasBeenClicked(true)
|
||||
onClick?.()
|
||||
}}
|
||||
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
|
||||
fullWidth
|
||||
{...buttonProps}
|
||||
{notices.length ? (
|
||||
<Tooltip
|
||||
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
|
||||
placement={notices[0].placement ?? 'right'}
|
||||
delayMs={0}
|
||||
visible={true}
|
||||
>
|
||||
{content}
|
||||
</LemonButton>
|
||||
</Tooltip>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={isNavCollapsedActually ? (here ? `${title} (you are here)` : title) : null}
|
||||
placement="right"
|
||||
delayMs={0}
|
||||
visible={!persistentTooltip && hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -19,9 +19,7 @@ import {
|
||||
IconHome,
|
||||
IconLive,
|
||||
IconPeople,
|
||||
IconPerson,
|
||||
IconPieChart,
|
||||
IconQuestion,
|
||||
IconRewindPlay,
|
||||
IconTestTube,
|
||||
IconToggle,
|
||||
@@ -32,8 +30,6 @@ import {
|
||||
IconChat,
|
||||
} from '@posthog/icons'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { annotationsSidebarLogic } from './sidebars/annotations'
|
||||
import { cohortsSidebarLogic } from './sidebars/cohorts'
|
||||
import { dashboardsSidebarLogic } from './sidebars/dashboards'
|
||||
import { dataManagementSidebarLogic } from './sidebars/dataManagement'
|
||||
import { experimentsSidebarLogic } from './sidebars/experiments'
|
||||
@@ -322,33 +318,12 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
|
||||
to: isUsingSidebar ? undefined : urls.eventDefinitions(),
|
||||
},
|
||||
{
|
||||
identifier: Scene.Persons,
|
||||
label: 'People and groups',
|
||||
icon: <IconPerson />,
|
||||
identifier: Scene.PersonsManagement,
|
||||
label: 'People',
|
||||
icon: <IconPeople />,
|
||||
logic: isUsingSidebar ? personsAndGroupsSidebarLogic : undefined,
|
||||
to: isUsingSidebar ? undefined : urls.persons(),
|
||||
},
|
||||
{
|
||||
identifier: Scene.Cohorts,
|
||||
label: 'Cohorts',
|
||||
icon: <IconPeople />,
|
||||
logic: isUsingSidebar ? cohortsSidebarLogic : undefined,
|
||||
to: isUsingSidebar ? undefined : urls.cohorts(),
|
||||
},
|
||||
{
|
||||
identifier: Scene.Annotations,
|
||||
label: 'Annotations',
|
||||
icon: <IconQuestion />,
|
||||
logic: isUsingSidebar ? annotationsSidebarLogic : undefined,
|
||||
to: isUsingSidebar ? undefined : urls.annotations(),
|
||||
},
|
||||
{
|
||||
identifier: Scene.ToolbarLaunch,
|
||||
label: 'Toolbar',
|
||||
icon: <IconToolbar />,
|
||||
logic: isUsingSidebar ? toolbarSidebarLogic : undefined,
|
||||
to: isUsingSidebar ? undefined : urls.toolbarLaunch(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -415,6 +390,13 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
|
||||
icon: <IconApps />,
|
||||
to: urls.projectApps(),
|
||||
},
|
||||
{
|
||||
identifier: Scene.ToolbarLaunch,
|
||||
label: 'Toolbar',
|
||||
icon: <IconToolbar />,
|
||||
logic: isUsingSidebar ? toolbarSidebarLogic : undefined,
|
||||
to: isUsingSidebar ? undefined : urls.toolbarLaunch(),
|
||||
},
|
||||
],
|
||||
]
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
|
||||
const blankScene = (): any => ({ scene: { component: () => null, logic: null } })
|
||||
const scenes: any = { [Scene.Annotations]: blankScene, [Scene.Dashboards]: blankScene }
|
||||
const scenes: any = { [Scene.SavedInsights]: blankScene, [Scene.Dashboards]: blankScene }
|
||||
|
||||
describe('breadcrumbsLogic', () => {
|
||||
let logic: ReturnType<typeof breadcrumbsLogic.build>
|
||||
@@ -24,9 +24,9 @@ describe('breadcrumbsLogic', () => {
|
||||
logic.mount()
|
||||
|
||||
// test with .delay because subscriptions happen async
|
||||
router.actions.push(urls.annotations())
|
||||
await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Annotations • PostHog' })
|
||||
expect(global.document.title).toEqual('Annotations • PostHog')
|
||||
router.actions.push(urls.savedInsights())
|
||||
await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Insights • PostHog' })
|
||||
expect(global.document.title).toEqual('Insights • PostHog')
|
||||
|
||||
router.actions.push(urls.dashboards())
|
||||
await expectLogic(logic).delay(1).toMatchValues({ documentTitle: 'Dashboards • PostHog' })
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Scene } from 'scenes/sceneTypes'
|
||||
import { LemonButton, LemonButtonProps, LemonButtonWithSideAction, SideAction } from 'lib/lemon-ui/LemonButton'
|
||||
import { sceneConfigurations } from 'scenes/scenes'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { SidebarChangeNoticeTooltip } from '~/layout/navigation/SideBar/SidebarChangeNotice'
|
||||
|
||||
export interface PageButtonProps extends Pick<LemonButtonProps, 'icon' | 'onClick' | 'to'> {
|
||||
/** Used for highlighting the active scene. `identifier` of type number means dashboard ID instead of scene. */
|
||||
@@ -32,48 +33,51 @@ export function PageButton({ title, sideAction, identifier, highlight, ...button
|
||||
|
||||
return (
|
||||
<li>
|
||||
{sideAction ? (
|
||||
<LemonButtonWithSideAction
|
||||
fullWidth
|
||||
status={buttonStatus}
|
||||
active={isActive}
|
||||
onClick={hideSideBarMobile}
|
||||
sideAction={{
|
||||
...sideAction,
|
||||
'data-attr': sideAction.identifier
|
||||
? `menu-item-${sideAction.identifier.toLowerCase()}`
|
||||
: undefined,
|
||||
}}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
{...buttonProps}
|
||||
>
|
||||
<span className="text-default">{title}</span>
|
||||
</LemonButtonWithSideAction>
|
||||
) : (
|
||||
<LemonButton
|
||||
fullWidth
|
||||
status={buttonStatus}
|
||||
active={isActive}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onClick={hideSideBarMobile}
|
||||
{...buttonProps}
|
||||
>
|
||||
<span className="text-default grow">{title}</span>
|
||||
{highlight === 'alpha' ? (
|
||||
<LemonTag type="completion" className="ml-1 float-right uppercase">
|
||||
Alpha
|
||||
</LemonTag>
|
||||
) : highlight === 'beta' ? (
|
||||
<LemonTag type="warning" className="ml-1 float-right uppercase">
|
||||
Beta
|
||||
</LemonTag>
|
||||
) : highlight === 'new' ? (
|
||||
<LemonTag type="success" className="ml-1 float-right uppercase">
|
||||
New
|
||||
</LemonTag>
|
||||
) : null}
|
||||
</LemonButton>
|
||||
)}
|
||||
<SidebarChangeNoticeTooltip identifier={identifier}>
|
||||
{sideAction ? (
|
||||
<LemonButtonWithSideAction
|
||||
fullWidth
|
||||
status={buttonStatus}
|
||||
active={isActive}
|
||||
onClick={hideSideBarMobile}
|
||||
sideAction={{
|
||||
...sideAction,
|
||||
'data-attr': sideAction.identifier
|
||||
? `menu-item-${sideAction.identifier.toLowerCase()}`
|
||||
: undefined,
|
||||
}}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
{...buttonProps}
|
||||
>
|
||||
<span className="text-default">{title}</span>
|
||||
</LemonButtonWithSideAction>
|
||||
) : (
|
||||
<LemonButton
|
||||
fullWidth
|
||||
status={buttonStatus}
|
||||
active={isActive}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onClick={hideSideBarMobile}
|
||||
sideIcon={null}
|
||||
{...buttonProps}
|
||||
>
|
||||
<span className="text-default grow">{title}</span>
|
||||
{highlight === 'alpha' ? (
|
||||
<LemonTag type="completion" className="ml-1 float-right uppercase">
|
||||
Alpha
|
||||
</LemonTag>
|
||||
) : highlight === 'beta' ? (
|
||||
<LemonTag type="warning" className="ml-1 float-right uppercase">
|
||||
Beta
|
||||
</LemonTag>
|
||||
) : highlight === 'new' ? (
|
||||
<LemonTag type="success" className="ml-1 float-right uppercase">
|
||||
New
|
||||
</LemonTag>
|
||||
) : null}
|
||||
</LemonButton>
|
||||
)}
|
||||
</SidebarChangeNoticeTooltip>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
IconApps,
|
||||
IconBarChart,
|
||||
IconCohort,
|
||||
IconComment,
|
||||
IconDatabase,
|
||||
IconExperiment,
|
||||
IconFlag,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
IconLive,
|
||||
IconMessages,
|
||||
IconOpenInApp,
|
||||
IconPerson,
|
||||
IconPinOutline,
|
||||
IconPipeline,
|
||||
IconPlus,
|
||||
@@ -39,7 +37,6 @@ import { AvailableFeature } from '~/types'
|
||||
import './SideBar.scss'
|
||||
import { navigationLogic } from '../navigationLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { groupsModel } from '~/models/groupsModel'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
|
||||
import { SideBarApps } from '~/layout/navigation/SideBar/SideBarApps'
|
||||
@@ -60,7 +57,6 @@ function Pages(): JSX.Element {
|
||||
const { hideSideBarMobile, toggleProjectSwitcher, hideProjectSwitcher } = useActions(navigationLogic)
|
||||
const { isProjectSwitcherShown } = useValues(navigationLogic)
|
||||
const { pinnedDashboards } = useValues(dashboardsModel)
|
||||
const { showGroupsOptions } = useValues(groupsModel)
|
||||
const { hasAvailableFeature } = useValues(userLogic)
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
@@ -222,10 +218,10 @@ function Pages(): JSX.Element {
|
||||
to={urls.eventDefinitions()}
|
||||
/>
|
||||
<PageButton
|
||||
icon={<IconPerson />}
|
||||
identifier={Scene.Persons}
|
||||
icon={<IconCohort />}
|
||||
identifier={Scene.PersonsManagement}
|
||||
to={urls.persons()}
|
||||
title={`Persons${showGroupsOptions ? ' & Groups' : ''}`}
|
||||
title="People"
|
||||
/>
|
||||
<FlaggedFeature flag={FEATURE_FLAGS.PIPELINE_UI}>
|
||||
<PageButton icon={<IconPipeline />} identifier={Scene.Pipeline} to={urls.pipeline()} />
|
||||
@@ -239,8 +235,6 @@ function Pages(): JSX.Element {
|
||||
highlight="beta"
|
||||
/>
|
||||
</FlaggedFeature>
|
||||
<PageButton icon={<IconCohort />} identifier={Scene.Cohorts} to={urls.cohorts()} />
|
||||
<PageButton icon={<IconComment />} identifier={Scene.Annotations} to={urls.annotations()} />
|
||||
{canViewPlugins(currentOrganization) || Object.keys(frontendApps).length > 0 ? (
|
||||
<>
|
||||
<div className="SideBar__heading">Apps</div>
|
||||
|
||||
130
frontend/src/layout/navigation/SideBar/SidebarChangeNotice.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { IconCheck } from '@posthog/icons'
|
||||
import { LemonButton, LemonDivider, Tooltip, TooltipProps } from '@posthog/lemon-ui'
|
||||
import { useValues } from 'kea'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import posthog from 'posthog-js'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
|
||||
export type SidebarChangeNoticeProps = {
|
||||
identifier: string | number
|
||||
}
|
||||
|
||||
export type SidebarChangeNoticeTooltipProps = SidebarChangeNoticeProps & {
|
||||
children: TooltipProps['children']
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in combination with a feature flag like:
|
||||
*
|
||||
* sidebar-notice-annotations-2023-10-30
|
||||
* matching:
|
||||
* properties:
|
||||
* sidebar_notice/annotations-2023-10-30: doesn't equal true
|
||||
* joined_at: before 2023-10-30
|
||||
*
|
||||
*/
|
||||
|
||||
const NOTICES: {
|
||||
identifier: Scene
|
||||
description: React.ReactNode
|
||||
placement: TooltipProps['placement']
|
||||
flagSuffix: string
|
||||
}[] = [
|
||||
{
|
||||
identifier: Scene.DataManagement,
|
||||
description: (
|
||||
<>
|
||||
<b>Annotations</b> have moved here!
|
||||
<br />
|
||||
You can now find them in <b>Data Management</b>
|
||||
</>
|
||||
),
|
||||
placement: 'rightBottom',
|
||||
flagSuffix: 'annotations-2023-10-30',
|
||||
},
|
||||
{
|
||||
identifier: Scene.PersonsManagement,
|
||||
description: (
|
||||
<>
|
||||
<b>Cohorts</b> have moved here!
|
||||
<br />
|
||||
You can now find them in <b>People</b>
|
||||
</>
|
||||
),
|
||||
placement: 'rightTop',
|
||||
flagSuffix: 'cohorts-2023-10-30',
|
||||
},
|
||||
]
|
||||
|
||||
export function SidebarChangeNoticeContent({
|
||||
notices,
|
||||
onAcknowledged,
|
||||
}: {
|
||||
notices: typeof NOTICES
|
||||
onAcknowledged: () => void
|
||||
}): JSX.Element | null {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex-1">
|
||||
{notices.map((notice, i) => (
|
||||
<Fragment key={i}>
|
||||
{notice.description}
|
||||
{i < notices.length - 1 && <LemonDivider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<LemonButton size="small" onClick={onAcknowledged} icon={<IconCheck />} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSidebarChangeNotices({ identifier }: SidebarChangeNoticeProps): [typeof NOTICES, () => void] {
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
const [noticeAcknowledged, setNoticeAcknowledged] = useState(false)
|
||||
|
||||
const notices = NOTICES.filter((notice) => notice.identifier === identifier).filter(
|
||||
(notice) => featureFlags[`sidebar-notice-${notice.flagSuffix}`]
|
||||
)
|
||||
|
||||
const onAcknowledged = (): void => {
|
||||
notices.forEach((change) => {
|
||||
posthog.capture('sidebar notice acknowledged', {
|
||||
change: change.flagSuffix,
|
||||
$set: {
|
||||
[`sidebar_notice/${change.flagSuffix}`]: true,
|
||||
},
|
||||
})
|
||||
setNoticeAcknowledged(true)
|
||||
})
|
||||
}
|
||||
|
||||
return [!noticeAcknowledged ? notices : [], onAcknowledged]
|
||||
}
|
||||
|
||||
export function SidebarChangeNoticeTooltip({ identifier, children }: SidebarChangeNoticeTooltipProps): JSX.Element {
|
||||
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
|
||||
|
||||
if (!notices.length) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
visible
|
||||
placement={notices[0].placement}
|
||||
delayMs={0}
|
||||
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
|
||||
>
|
||||
{React.cloneElement(children as React.ReactElement, {
|
||||
onClick: () => {
|
||||
onAcknowledged()
|
||||
if (React.isValidElement(children)) {
|
||||
children.props.onClick?.()
|
||||
}
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -256,6 +256,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tooltip & {
|
||||
// Buttons have an overriden style in tooltips, as they are always dark
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
.LemonButton__icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.posthog-3000 & {
|
||||
font-size: 0.8125rem;
|
||||
border: none !important; // 3000 buttons never have borders
|
||||
|
||||
@@ -134,10 +134,7 @@ export const Link: React.FC<LinkProps & React.RefAttributes<HTMLElement>> = Reac
|
||||
{targetBlankIcon && target === '_blank' ? <IconOpenInNew /> : null}
|
||||
</a>
|
||||
) : (
|
||||
<Tooltip
|
||||
isDefaultTooltip
|
||||
title={disabledReason ? <span className="italic">{disabledReason}</span> : undefined}
|
||||
>
|
||||
<Tooltip title={disabledReason ? <span className="italic">{disabledReason}</span> : undefined}>
|
||||
<span>
|
||||
<button
|
||||
ref={ref as any}
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useDebounce } from 'use-debounce'
|
||||
const DEFAULT_DELAY_MS = 500
|
||||
|
||||
export type TooltipProps = AntdTooltipProps & {
|
||||
/** Whether Ant Design's default Tooltip behavior should be used instead of PostHog's. */
|
||||
isDefaultTooltip?: boolean
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
@@ -17,17 +15,11 @@ export type TooltipProps = AntdTooltipProps & {
|
||||
* See https://github.com/ant-design/ant-design/blob/master/components/tooltip/index.tsx#L82-L130.
|
||||
*/
|
||||
// CAUTION: Any changes here will affect tooltips across the entire app.
|
||||
export function Tooltip({
|
||||
children,
|
||||
visible,
|
||||
isDefaultTooltip = false,
|
||||
delayMs = DEFAULT_DELAY_MS,
|
||||
...props
|
||||
}: TooltipProps): JSX.Element {
|
||||
const [localVisible, setVisible] = useState(!!visible)
|
||||
export function Tooltip({ children, visible, delayMs = DEFAULT_DELAY_MS, ...props }: TooltipProps): JSX.Element {
|
||||
const [localVisible, setVisible] = useState(false)
|
||||
const [debouncedLocalVisible] = useDebounce(visible ?? localVisible, delayMs)
|
||||
|
||||
if (!isDefaultTooltip && !('mouseEnterDelay' in props)) {
|
||||
if (!('mouseEnterDelay' in props)) {
|
||||
// If not preserving default behavior and mouseEnterDelay is not already provided, we use a custom default here
|
||||
props.mouseEnterDelay = delayMs
|
||||
}
|
||||
@@ -36,16 +28,22 @@ export function Tooltip({
|
||||
// See https://github.com/ant-design/ant-design/blob/master/components/tooltip/index.tsx#L226
|
||||
const child = React.isValidElement(children) ? children : <span>{children}</span>
|
||||
|
||||
const derivedVisible = typeof visible === 'undefined' ? localVisible && debouncedLocalVisible : visible
|
||||
|
||||
return props.title ? (
|
||||
<AntdTooltip {...props} visible={isDefaultTooltip ? visible : localVisible && debouncedLocalVisible}>
|
||||
<AntdTooltip {...props} visible={derivedVisible}>
|
||||
{React.cloneElement(child, {
|
||||
onMouseEnter: () => {
|
||||
child.props.onMouseEnter?.()
|
||||
setVisible(true)
|
||||
if (typeof visible === 'undefined') {
|
||||
setVisible(true)
|
||||
}
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
child.props.onMouseLeave?.()
|
||||
setVisible(false)
|
||||
if (typeof visible === 'undefined') {
|
||||
setVisible(false)
|
||||
}
|
||||
},
|
||||
})}
|
||||
</AntdTooltip>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { kea, path, connect, selectors, events } from 'kea'
|
||||
import { kea, path, connect, selectors, afterMount } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { GroupType, GroupTypeIndex } from '~/types'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
@@ -105,7 +105,7 @@ export const groupsModel = kea<groupsModelType>([
|
||||
}
|
||||
},
|
||||
})),
|
||||
events(({ actions }) => ({
|
||||
afterMount: actions.loadAllGroupTypes,
|
||||
})),
|
||||
afterMount(({ actions }) => {
|
||||
actions.loadAllGroupTypes()
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -5,9 +5,10 @@ import { cohortsModel } from './cohortsModel'
|
||||
import { dashboardsModel } from './dashboardsModel'
|
||||
import { propertyDefinitionsModel } from './propertyDefinitionsModel'
|
||||
import type { modelsType } from './indexType'
|
||||
import { groupsModel } from './groupsModel'
|
||||
|
||||
/** "Models" are logics that are persistently mounted (start with app) */
|
||||
export const models = kea<modelsType>([
|
||||
path(['models', 'index']),
|
||||
connect([actionsModel, annotationsModel, cohortsModel, dashboardsModel, propertyDefinitionsModel]),
|
||||
connect([actionsModel, annotationsModel, cohortsModel, dashboardsModel, propertyDefinitionsModel, groupsModel]),
|
||||
])
|
||||
|
||||
@@ -9,6 +9,15 @@ import { IconWarning } from 'lib/lemon-ui/icons'
|
||||
import { shortTimeZone } from 'lib/utils'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
export function NewAnnotationButton(): JSX.Element {
|
||||
const { openModalToCreateAnnotation } = useActions(annotationModalLogic)
|
||||
return (
|
||||
<LemonButton type="primary" data-attr="create-annotation" onClick={() => openModalToCreateAnnotation()}>
|
||||
New annotation
|
||||
</LemonButton>
|
||||
)
|
||||
}
|
||||
|
||||
export function AnnotationModal({
|
||||
overlayRef,
|
||||
contentRef,
|
||||
|
||||
@@ -5,9 +5,7 @@ import {
|
||||
annotationModalLogic,
|
||||
ANNOTATION_DAYJS_FORMAT,
|
||||
} from './annotationModalLogic'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { AnnotationScope, InsightShortId, AnnotationType, ProductKey } from '~/types'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { LemonTable, LemonTableColumns, LemonTableColumn } from 'lib/lemon-ui/LemonTable'
|
||||
import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
|
||||
import { LemonButton } from 'lib/lemon-ui/LemonButton'
|
||||
@@ -24,11 +22,6 @@ import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
|
||||
import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction'
|
||||
import { MicrophoneHog } from 'lib/components/hedgehogs'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Annotations,
|
||||
logic: annotationModalLogic,
|
||||
}
|
||||
|
||||
export function Annotations(): JSX.Element {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { currentOrganization } = useValues(organizationLogic)
|
||||
@@ -135,26 +128,10 @@ export function Annotations(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Annotations"
|
||||
caption={
|
||||
!shouldShowEmptyState && !shouldShowProductIntroduction ? (
|
||||
<>
|
||||
Annotations allow you to mark when certain changes happened so you can easily see how they
|
||||
impacted your metrics.
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
buttons={
|
||||
<LemonButton
|
||||
type="primary"
|
||||
data-attr="create-annotation"
|
||||
onClick={() => openModalToCreateAnnotation()}
|
||||
>
|
||||
New annotation
|
||||
</LemonButton>
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
Annotations allow you to mark when certain changes happened so you can easily see how they impacted your
|
||||
metrics.
|
||||
</p>
|
||||
<div data-attr={'annotations-content'}>
|
||||
{(shouldShowEmptyState || shouldShowProductIntroduction) && (
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -9,10 +9,8 @@ import { teamLogic } from 'scenes/teamLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { actionToUrl, urlToAction } from 'kea-router'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { urls } from '@posthog/apps-common'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { urlToAction } from 'kea-router'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
export const ANNOTATION_DAYJS_FORMAT = 'MMMM DD, YYYY h:mm A'
|
||||
|
||||
@@ -176,7 +174,7 @@ export const annotationModalLogic = kea<annotationModalLogicType>([
|
||||
},
|
||||
})),
|
||||
urlToAction(({ values, actions, cache }) => ({
|
||||
'/annotations/:id': ({ id }) => {
|
||||
[urls.annotation(':id')]: ({ id }) => {
|
||||
cache.annotationToShowId = parseInt(id as string)
|
||||
const annotation = values.annotations.find((a) => a.id === cache.annotationToShowId)
|
||||
if (!annotation) {
|
||||
@@ -185,7 +183,4 @@ export const annotationModalLogic = kea<annotationModalLogicType>([
|
||||
actions.openModalToEditAnnotation(annotation)
|
||||
},
|
||||
})),
|
||||
actionToUrl(() => ({
|
||||
closeModal: () => (sceneLogic.values.scene === Scene.Annotations ? urls.annotations() : undefined),
|
||||
})),
|
||||
])
|
||||
|
||||
@@ -9,27 +9,20 @@ export const appScenes: Record<Scene, () => any> = {
|
||||
[Scene.Dashboard]: () => import('./dashboard/Dashboard'),
|
||||
[Scene.Insight]: () => import('./insights/InsightScene'),
|
||||
[Scene.WebAnalytics]: () => import('./web-analytics/WebAnalyticsScene'),
|
||||
[Scene.Cohorts]: () => import('./cohorts/Cohorts'),
|
||||
[Scene.Cohort]: () => import('./cohorts/Cohort'),
|
||||
[Scene.DataManagement]: () => import('./data-management/events/EventDefinitionsTable'),
|
||||
[Scene.DataManagement]: () => import('./data-management/DataManagementScene'),
|
||||
[Scene.Events]: () => import('./events/Events'),
|
||||
[Scene.BatchExports]: () => import('./batch_exports/BatchExportsListScene'),
|
||||
[Scene.BatchExportEdit]: () => import('./batch_exports/BatchExportEditScene'),
|
||||
[Scene.BatchExport]: () => import('./batch_exports/BatchExportScene'),
|
||||
[Scene.Actions]: () => import('./actions/ActionsTable'),
|
||||
[Scene.EventDefinitions]: () => import('./data-management/events/EventDefinitionsTable'),
|
||||
[Scene.EventDefinition]: () => import('./data-management/definition/DefinitionView'),
|
||||
[Scene.PropertyDefinitions]: () => import('./data-management/properties/PropertyDefinitionsTable'),
|
||||
[Scene.PropertyDefinition]: () => import('./data-management/definition/DefinitionView'),
|
||||
[Scene.DataManagementHistory]: () => import('./data-management/history/History'),
|
||||
[Scene.Database]: () => import('./data-management/database/DatabaseScene'),
|
||||
[Scene.Replay]: () => import('./session-recordings/SessionRecordings'),
|
||||
[Scene.ReplaySingle]: () => import('./session-recordings/detail/SessionRecordingDetail'),
|
||||
[Scene.ReplayPlaylist]: () => import('./session-recordings/playlist/SessionRecordingsPlaylistScene'),
|
||||
[Scene.PersonsManagement]: () => import('./persons-management/PersonsManagementScene'),
|
||||
[Scene.Person]: () => import('./persons/PersonScene'),
|
||||
[Scene.Persons]: () => import('./persons/PersonsScene'),
|
||||
[Scene.Pipeline]: () => import('./pipeline/Pipeline'),
|
||||
[Scene.Groups]: () => import('./groups/Groups'),
|
||||
[Scene.Group]: () => import('./groups/Group'),
|
||||
[Scene.Action]: () => import('./actions/Action'),
|
||||
[Scene.Experiments]: () => import('./experiments/Experiments'),
|
||||
@@ -59,7 +52,6 @@ export const appScenes: Record<Scene, () => any> = {
|
||||
[Scene.AsyncMigrations]: () => import('./instance/AsyncMigrations/AsyncMigrations'),
|
||||
[Scene.DeadLetterQueue]: () => import('./instance/DeadLetterQueue/DeadLetterQueue'),
|
||||
[Scene.MySettings]: () => import('./me/Settings'),
|
||||
[Scene.Annotations]: () => import('./annotations/Annotations'),
|
||||
[Scene.PreflightCheck]: () => import('./PreflightCheck/PreflightCheck'),
|
||||
[Scene.Signup]: () => import('./authentication/signup/SignupContainer'),
|
||||
[Scene.InviteSignup]: () => import('./authentication/InviteSignup'),
|
||||
@@ -75,7 +67,6 @@ export const appScenes: Record<Scene, () => any> = {
|
||||
[Scene.PasswordResetComplete]: () => import('./authentication/PasswordResetComplete'),
|
||||
[Scene.Unsubscribe]: () => import('./Unsubscribe/Unsubscribe'),
|
||||
[Scene.IntegrationsRedirect]: () => import('./IntegrationsRedirect/IntegrationsRedirect'),
|
||||
[Scene.IngestionWarnings]: () => import('./data-management/ingestion-warnings/IngestionWarningsView'),
|
||||
[Scene.DebugQuery]: () => import('./debug/DebugScene'),
|
||||
[Scene.VerifyEmail]: () => import('./authentication/signup/verify-email/VerifyEmail'),
|
||||
[Scene.Feedback]: () => import('./feedback/Feedback'),
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { cohortsModel } from '../../models/cohortsModel'
|
||||
import { useValues, useActions } from 'kea'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { AvailableFeature, CohortType, ProductKey } from '~/types'
|
||||
import './Cohorts.scss'
|
||||
import Fuse from 'fuse.js'
|
||||
import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
|
||||
import { Link } from 'lib/lemon-ui/Link'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import { Spinner } from 'lib/lemon-ui/Spinner/Spinner'
|
||||
import { urls } from 'scenes/urls'
|
||||
@@ -160,10 +158,6 @@ export function Cohorts(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Cohorts"
|
||||
caption="Create lists of users who have something in common to use in analytics or feature flags."
|
||||
/>
|
||||
{(shouldShowProductIntroduction || shouldShowEmptyState) && (
|
||||
<ProductIntroduction
|
||||
productName="Cohorts"
|
||||
@@ -185,13 +179,6 @@ export function Cohorts(): JSX.Element {
|
||||
onChange={setSearchTerm}
|
||||
value={searchTerm}
|
||||
/>
|
||||
<LemonButton
|
||||
type="primary"
|
||||
data-attr="create-cohort"
|
||||
onClick={() => router.actions.push(urls.cohort('new'))}
|
||||
>
|
||||
New Cohort
|
||||
</LemonButton>
|
||||
</div>
|
||||
<LemonTable
|
||||
columns={columns}
|
||||
@@ -207,8 +194,3 @@ export function Cohorts(): JSX.Element {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: Cohorts,
|
||||
logic: cohortsModel,
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { actionToUrl, urlToAction } from 'kea-router'
|
||||
import { kea, useActions, useValues, path, connect, actions, reducers, selectors } from 'kea'
|
||||
import { urls } from 'scenes/urls'
|
||||
import type { eventsTabsLogicType } from './DataManagementPageTabsType'
|
||||
import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import { IconInfo } from 'lib/lemon-ui/icons'
|
||||
import { TitleWithIcon } from 'lib/components/TitleWithIcon'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { LemonTabs } from 'lib/lemon-ui/LemonTabs'
|
||||
|
||||
export enum DataManagementTab {
|
||||
Actions = 'actions',
|
||||
EventDefinitions = 'events',
|
||||
PropertyDefinitions = 'properties',
|
||||
History = 'history',
|
||||
IngestionWarnings = 'warnings',
|
||||
Database = 'database',
|
||||
}
|
||||
|
||||
const tabUrls = {
|
||||
[DataManagementTab.PropertyDefinitions]: urls.propertyDefinitions(),
|
||||
[DataManagementTab.EventDefinitions]: urls.eventDefinitions(),
|
||||
[DataManagementTab.Actions]: urls.actions(),
|
||||
[DataManagementTab.History]: urls.dataManagementHistory(),
|
||||
[DataManagementTab.IngestionWarnings]: urls.ingestionWarnings(),
|
||||
[DataManagementTab.Database]: urls.database(),
|
||||
}
|
||||
|
||||
const eventsTabsLogic = kea<eventsTabsLogicType>([
|
||||
path(['scenes', 'events', 'eventsTabsLogic']),
|
||||
connect({
|
||||
values: [featureFlagLogic, ['featureFlags']],
|
||||
}),
|
||||
actions({
|
||||
setTab: (tab: DataManagementTab) => ({ tab }),
|
||||
}),
|
||||
reducers({
|
||||
tab: [
|
||||
DataManagementTab.EventDefinitions as DataManagementTab,
|
||||
{
|
||||
setTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors({
|
||||
showWarningsTab: [
|
||||
(s) => [s.featureFlags],
|
||||
(featureFlags): boolean => !!featureFlags[FEATURE_FLAGS.INGESTION_WARNINGS_ENABLED],
|
||||
],
|
||||
}),
|
||||
actionToUrl(() => ({
|
||||
setTab: ({ tab }) => tabUrls[tab as DataManagementTab] || urls.events(),
|
||||
})),
|
||||
urlToAction(({ actions, values }) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tabUrls).map(([key, url]) => [
|
||||
url,
|
||||
() => {
|
||||
if (values.tab !== key) {
|
||||
actions.setTab(key as DataManagementTab)
|
||||
}
|
||||
},
|
||||
])
|
||||
)
|
||||
}),
|
||||
])
|
||||
|
||||
export function DataManagementPageTabs({ tab }: { tab: DataManagementTab }): JSX.Element {
|
||||
const { showWarningsTab } = useValues(eventsTabsLogic)
|
||||
const { setTab } = useActions(eventsTabsLogic)
|
||||
|
||||
return (
|
||||
<>
|
||||
<LemonTabs
|
||||
activeKey={tab}
|
||||
onChange={(t) => setTab(t)}
|
||||
tabs={[
|
||||
{
|
||||
key: DataManagementTab.EventDefinitions,
|
||||
label: <span data-attr="data-management-events-tab">Events</span>,
|
||||
},
|
||||
{
|
||||
key: DataManagementTab.Actions,
|
||||
label: (
|
||||
<TitleWithIcon
|
||||
icon={
|
||||
<Tooltip title="Actions consist of one or more events that you have decided to put into a deliberately-labeled bucket. They're used in insights and dashboards.">
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
}
|
||||
data-attr="data-management-actions-tab"
|
||||
>
|
||||
Actions
|
||||
</TitleWithIcon>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DataManagementTab.PropertyDefinitions,
|
||||
label: (
|
||||
<TitleWithIcon
|
||||
icon={
|
||||
<Tooltip title="Properties are additional data sent along with an event capture. Use properties to understand additional information about events and the actors that generate them.">
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
}
|
||||
data-attr="data-management-event-properties-tab"
|
||||
>
|
||||
Properties
|
||||
</TitleWithIcon>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DataManagementTab.History,
|
||||
label: <span data-attr="data-management-history-tab">History</span>,
|
||||
},
|
||||
|
||||
showWarningsTab && {
|
||||
key: DataManagementTab.IngestionWarnings,
|
||||
label: <span data-attr="data-management-warnings-tab">Ingestion Warnings</span>,
|
||||
},
|
||||
|
||||
{
|
||||
key: DataManagementTab.Database,
|
||||
label: (
|
||||
<span data-attr="data-management-database-tab">
|
||||
Database
|
||||
<LemonTag type="warning" className="uppercase ml-2">
|
||||
Beta
|
||||
</LemonTag>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mswDecorator } from '~/mocks/browser'
|
||||
import { mswDecorator, setFeatureFlags } from '~/mocks/browser'
|
||||
import { Meta } from '@storybook/react'
|
||||
import { useAvailableFeatures } from '~/mocks/features'
|
||||
import { AvailableFeature } from '~/types'
|
||||
@@ -9,6 +9,7 @@ import { App } from 'scenes/App'
|
||||
import { DatabaseSchemaQueryResponse } from '~/queries/schema'
|
||||
import { ingestionWarningsResponse } from './ingestion-warnings/__mocks__/ingestion-warnings-response'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
const MOCK_DATABASE: DatabaseSchemaQueryResponse = {
|
||||
events: [
|
||||
@@ -116,6 +117,7 @@ export function Database(): JSX.Element {
|
||||
}
|
||||
|
||||
export function IngestionWarnings(): JSX.Element {
|
||||
setFeatureFlags([FEATURE_FLAGS.INGESTION_WARNINGS_ENABLED])
|
||||
useEffect(() => {
|
||||
router.actions.push(urls.ingestionWarnings())
|
||||
}, [])
|
||||
|
||||
207
frontend/src/scenes/data-management/DataManagementScene.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { actions, connect, kea, path, reducers, selectors, useActions, useValues } from 'kea'
|
||||
import { actionToUrl, urlToAction } from 'kea-router'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import { IconInfo } from 'lib/lemon-ui/icons'
|
||||
import { TitleWithIcon } from 'lib/components/TitleWithIcon'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs'
|
||||
import React from 'react'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { NewActionButton } from 'scenes/actions/NewActionButton'
|
||||
import { Annotations } from 'scenes/annotations'
|
||||
|
||||
import type { dataManagementSceneLogicType } from './DataManagementSceneType'
|
||||
import { NewAnnotationButton } from 'scenes/annotations/AnnotationModal'
|
||||
import { EventDefinitionsTable } from './events/EventDefinitionsTable'
|
||||
import { ActionsTable } from './actions/ActionsTable'
|
||||
import { PropertyDefinitionsTable } from './properties/PropertyDefinitionsTable'
|
||||
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
|
||||
import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity'
|
||||
import { IngestionWarningsView } from './ingestion-warnings/IngestionWarningsView'
|
||||
import { DatabaseTableList } from './database/DatabaseTableList'
|
||||
import { Breadcrumb } from '~/types'
|
||||
import { capitalizeFirstLetter } from 'lib/utils'
|
||||
|
||||
export enum DataManagementTab {
|
||||
Actions = 'actions',
|
||||
EventDefinitions = 'events',
|
||||
PropertyDefinitions = 'properties',
|
||||
Annotations = 'annotations',
|
||||
History = 'history',
|
||||
IngestionWarnings = 'warnings',
|
||||
Database = 'database',
|
||||
}
|
||||
|
||||
const tabs: Record<
|
||||
DataManagementTab,
|
||||
{ url: string; label: LemonTab<any>['label']; content: JSX.Element; buttons?: React.ReactNode }
|
||||
> = {
|
||||
[DataManagementTab.EventDefinitions]: {
|
||||
url: urls.eventDefinitions(),
|
||||
label: 'Events',
|
||||
content: <EventDefinitionsTable />,
|
||||
},
|
||||
[DataManagementTab.Actions]: {
|
||||
url: urls.actions(),
|
||||
label: (
|
||||
<TitleWithIcon
|
||||
icon={
|
||||
<Tooltip title="Actions consist of one or more events that you have decided to put into a deliberately-labeled bucket. They're used in insights and dashboards.">
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
Actions
|
||||
</TitleWithIcon>
|
||||
),
|
||||
buttons: <NewActionButton />,
|
||||
content: <ActionsTable />,
|
||||
},
|
||||
[DataManagementTab.PropertyDefinitions]: {
|
||||
url: urls.propertyDefinitions(),
|
||||
label: (
|
||||
<TitleWithIcon
|
||||
icon={
|
||||
<Tooltip title="Properties are additional data sent along with an event capture. Use properties to understand additional information about events and the actors that generate them.">
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
Properties
|
||||
</TitleWithIcon>
|
||||
),
|
||||
content: <PropertyDefinitionsTable />,
|
||||
},
|
||||
[DataManagementTab.Annotations]: {
|
||||
url: urls.annotations(),
|
||||
content: <Annotations />,
|
||||
label: 'Annotations',
|
||||
buttons: <NewAnnotationButton />,
|
||||
},
|
||||
[DataManagementTab.History]: {
|
||||
url: urls.dataManagementHistory(),
|
||||
label: 'History',
|
||||
content: (
|
||||
<ActivityLog
|
||||
scope={ActivityScope.DATA_MANAGEMENT}
|
||||
caption={
|
||||
'Only actions taken in the UI are captured in History. Automatic creation of definitions by ingestion is not shown here.'
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
[DataManagementTab.IngestionWarnings]: {
|
||||
url: urls.ingestionWarnings(),
|
||||
label: 'Ingestion Warnings',
|
||||
content: <IngestionWarningsView />,
|
||||
},
|
||||
[DataManagementTab.Database]: {
|
||||
url: urls.database(),
|
||||
label: (
|
||||
<>
|
||||
Database
|
||||
<LemonTag type="warning" className="uppercase ml-2">
|
||||
Beta
|
||||
</LemonTag>
|
||||
</>
|
||||
),
|
||||
content: <DatabaseTableList />,
|
||||
},
|
||||
}
|
||||
|
||||
const dataManagementSceneLogic = kea<dataManagementSceneLogicType>([
|
||||
path(['scenes', 'events', 'dataManagementSceneLogic']),
|
||||
connect({
|
||||
values: [featureFlagLogic, ['featureFlags']],
|
||||
}),
|
||||
actions({
|
||||
setTab: (tab: DataManagementTab) => ({ tab }),
|
||||
}),
|
||||
reducers({
|
||||
tab: [
|
||||
DataManagementTab.EventDefinitions as DataManagementTab,
|
||||
{
|
||||
setTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors({
|
||||
breadcrumbs: [
|
||||
(s) => [s.tab],
|
||||
(tab): Breadcrumb[] => {
|
||||
return [
|
||||
{
|
||||
name: `Data Management`,
|
||||
path: tabs.events.url,
|
||||
},
|
||||
{
|
||||
name: capitalizeFirstLetter(tab),
|
||||
path: tabs[tab].url,
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
showWarningsTab: [
|
||||
(s) => [s.featureFlags],
|
||||
(featureFlags): boolean => !!featureFlags[FEATURE_FLAGS.INGESTION_WARNINGS_ENABLED],
|
||||
],
|
||||
enabledTabs: [
|
||||
(s) => [s.showWarningsTab],
|
||||
(showWarningsTab): DataManagementTab[] => {
|
||||
const allTabs = Object.keys(tabs)
|
||||
|
||||
return allTabs.filter((x) => {
|
||||
return x === DataManagementTab.IngestionWarnings ? showWarningsTab : true
|
||||
}) as DataManagementTab[]
|
||||
},
|
||||
],
|
||||
}),
|
||||
actionToUrl(() => ({
|
||||
setTab: ({ tab }) => tabs[tab as DataManagementTab]?.url || tabs.events.url,
|
||||
})),
|
||||
urlToAction(({ actions, values }) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tabs).map(([key, tab]) => [
|
||||
tab.url,
|
||||
() => {
|
||||
if (values.tab !== key) {
|
||||
actions.setTab(key as DataManagementTab)
|
||||
}
|
||||
},
|
||||
])
|
||||
)
|
||||
}),
|
||||
])
|
||||
|
||||
export function DataManagementScene(): JSX.Element {
|
||||
const { enabledTabs, tab } = useValues(dataManagementSceneLogic)
|
||||
const { setTab } = useActions(dataManagementSceneLogic)
|
||||
|
||||
const lemonTabs: LemonTab<DataManagementTab>[] = enabledTabs.map((key) => ({
|
||||
key: key as DataManagementTab,
|
||||
label: <span data-attr={`data-management-${key}-tab`}>{tabs[key].label}</span>,
|
||||
content: tabs[key].content,
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
buttons={<>{tabs[tab].buttons}</>}
|
||||
/>
|
||||
|
||||
<LemonTabs activeKey={tab} onChange={(t) => setTab(t)} tabs={lemonTabs} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: DataManagementScene,
|
||||
logic: dataManagementSceneLogic,
|
||||
}
|
||||
@@ -3,13 +3,12 @@ import { Radio } from 'antd'
|
||||
import { deleteWithUndo, stripHTTP } from 'lib/utils'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { actionsModel } from '~/models/actionsModel'
|
||||
import { NewActionButton } from './NewActionButton'
|
||||
import { NewActionButton } from '../../actions/NewActionButton'
|
||||
import { ActionType, AvailableFeature, ChartDisplayType, InsightType, ProductKey } from '~/types'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { teamLogic } from '../teamLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { teamLogic } from '../../teamLogic'
|
||||
import api from 'lib/api'
|
||||
import { urls } from '../urls'
|
||||
import { urls } from '../../urls'
|
||||
import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
|
||||
import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable/types'
|
||||
import { LemonTable } from 'lib/lemon-ui/LemonTable'
|
||||
@@ -18,19 +17,12 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
|
||||
import { More } from 'lib/lemon-ui/LemonButton/More'
|
||||
import { combineUrl } from 'kea-router'
|
||||
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { LemonInput } from '@posthog/lemon-ui'
|
||||
import { actionsLogic } from 'scenes/actions/actionsLogic'
|
||||
import { IconCheckmark, IconPlayCircle } from 'lib/lemon-ui/icons'
|
||||
import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction'
|
||||
import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: ActionsTable,
|
||||
logic: actionsLogic,
|
||||
}
|
||||
|
||||
export function ActionsTable(): JSX.Element {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { actionsLoading } = useValues(actionsModel({ params: 'include_count=1' }))
|
||||
@@ -230,13 +222,6 @@ export function ActionsTable(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div data-attr="manage-events-table">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
buttons={<NewActionButton />}
|
||||
/>
|
||||
<DataManagementPageTabs tab={DataManagementTab.Actions} />
|
||||
{(shouldShowEmptyState || shouldShowProductIntroduction) && (
|
||||
<ProductIntroduction
|
||||
productName="Actions"
|
||||
@@ -1,41 +0,0 @@
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { databaseSceneLogic } from './databaseSceneLogic'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { LemonInput, Link } from '@posthog/lemon-ui'
|
||||
import { DatabaseTablesContainer } from 'scenes/data-management/database/DatabaseTables'
|
||||
|
||||
export function DatabaseScene(): JSX.Element {
|
||||
const { searchTerm } = useValues(databaseSceneLogic)
|
||||
const { setSearchTerm } = useActions(databaseSceneLogic)
|
||||
|
||||
return (
|
||||
<div data-attr="database-scene">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
/>
|
||||
<DataManagementPageTabs tab={DataManagementTab.Database} />
|
||||
<div className="flex items-center justify-between gap-2 mb-4">
|
||||
<LemonInput type="search" placeholder="Search for tables" onChange={setSearchTerm} value={searchTerm} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 mb-4">
|
||||
<div>
|
||||
These are the database tables you can query under SQL insights with{' '}
|
||||
<Link to="https://posthog.com/manual/hogql" target="_blank">
|
||||
HogQL
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<DatabaseTablesContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: DatabaseScene,
|
||||
logic: databaseSceneLogic,
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import { LemonTable } from 'lib/lemon-ui/LemonTable'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { Link } from 'lib/lemon-ui/Link'
|
||||
import { ViewLinkDeleteButton } from 'scenes/data-warehouse/ViewLinkModal'
|
||||
import { DatabaseSceneRow } from 'scenes/data-warehouse/types'
|
||||
import { DatabaseTableListRow } from 'scenes/data-warehouse/types'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
interface DatabaseTableProps {
|
||||
table: string
|
||||
tables: DatabaseSceneRow[]
|
||||
tables: DatabaseTableListRow[]
|
||||
}
|
||||
|
||||
export function DatabaseTable({ table, tables }: DatabaseTableProps): JSX.Element {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { databaseTableListLogic } from './databaseTableListLogic'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { LemonInput, Link } from '@posthog/lemon-ui'
|
||||
import { DatabaseTablesContainer } from 'scenes/data-management/database/DatabaseTables'
|
||||
|
||||
export function DatabaseTableList(): JSX.Element {
|
||||
const { searchTerm } = useValues(databaseTableListLogic)
|
||||
const { setSearchTerm } = useActions(databaseTableListLogic)
|
||||
|
||||
return (
|
||||
<div data-attr="database-list">
|
||||
<div className="flex items-center justify-between gap-2 mb-4">
|
||||
<LemonInput type="search" placeholder="Search for tables" onChange={setSearchTerm} value={searchTerm} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 mb-4">
|
||||
<div>
|
||||
These are the database tables you can query under SQL insights with{' '}
|
||||
<Link to="https://posthog.com/manual/hogql" target="_blank">
|
||||
HogQL
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<DatabaseTablesContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { databaseSceneLogic, DatabaseSceneRow } from 'scenes/data-management/database/databaseSceneLogic'
|
||||
import { databaseTableListLogic, DatabaseTableListRow } from 'scenes/data-management/database/databaseTableListLogic'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { LemonButton, Link } from '@posthog/lemon-ui'
|
||||
import { urls } from 'scenes/urls'
|
||||
@@ -12,7 +12,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
export function DatabaseTablesContainer(): JSX.Element {
|
||||
const { filteredTables, databaseLoading } = useValues(databaseSceneLogic)
|
||||
const { filteredTables, databaseLoading } = useValues(databaseTableListLogic)
|
||||
const { toggleFieldModal, selectTableName } = useActions(viewLinkLogic)
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DatabaseTablesContainer(): JSX.Element {
|
||||
<DatabaseTables
|
||||
tables={filteredTables}
|
||||
loading={databaseLoading}
|
||||
renderRow={(row: DatabaseSceneRow) => {
|
||||
renderRow={(row: DatabaseTableListRow) => {
|
||||
return (
|
||||
<div className="px-4 py-3">
|
||||
<div className="mt-2">
|
||||
@@ -59,7 +59,7 @@ interface DatabaseTablesProps<T extends Record<string, any>> {
|
||||
extraColumns?: LemonTableColumns<T>
|
||||
}
|
||||
|
||||
export function DatabaseTables<T extends DatabaseSceneRow>({
|
||||
export function DatabaseTables<T extends DatabaseTableListRow>({
|
||||
tables,
|
||||
loading,
|
||||
renderRow,
|
||||
|
||||
@@ -2,16 +2,17 @@ import { actions, afterMount, kea, path, reducers, selectors } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import { query } from '~/queries/query'
|
||||
|
||||
import type { databaseSceneLogicType } from './databaseSceneLogicType'
|
||||
import { DatabaseSchemaQuery, DatabaseSchemaQueryResponseField, NodeKind } from '~/queries/schema'
|
||||
|
||||
export interface DatabaseSceneRow {
|
||||
import type { databaseTableListLogicType } from './databaseTableListLogicType'
|
||||
|
||||
export interface DatabaseTableListRow {
|
||||
name: string
|
||||
columns: DatabaseSchemaQueryResponseField[]
|
||||
}
|
||||
|
||||
export const databaseSceneLogic = kea<databaseSceneLogicType>([
|
||||
path(['scenes', 'data-management', 'database', 'databaseSceneLogic']),
|
||||
export const databaseTableListLogic = kea<databaseTableListLogicType>([
|
||||
path(['scenes', 'data-management', 'database', 'databaseTableListLogic']),
|
||||
actions({
|
||||
setSearchTerm: (searchTerm: string) => ({ searchTerm }),
|
||||
}),
|
||||
@@ -28,7 +29,7 @@ export const databaseSceneLogic = kea<databaseSceneLogicType>([
|
||||
selectors({
|
||||
filteredTables: [
|
||||
(s) => [s.database, s.searchTerm],
|
||||
(database, searchTerm): DatabaseSceneRow[] => {
|
||||
(database, searchTerm): DatabaseTableListRow[] => {
|
||||
if (!database) {
|
||||
return []
|
||||
}
|
||||
@@ -39,7 +40,7 @@ export const databaseSceneLogic = kea<databaseSceneLogicType>([
|
||||
({
|
||||
name: key,
|
||||
columns: value,
|
||||
} as DatabaseSceneRow)
|
||||
} as DatabaseTableListRow)
|
||||
)
|
||||
.filter(({ name }) => name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
@@ -47,7 +48,7 @@ export const databaseSceneLogic = kea<databaseSceneLogicType>([
|
||||
],
|
||||
tableOptions: [
|
||||
(s) => [s.filteredTables],
|
||||
(filteredTables: DatabaseSceneRow[]) =>
|
||||
(filteredTables: DatabaseTableListRow[]) =>
|
||||
filteredTables.map((row) => ({
|
||||
value: row.name,
|
||||
label: row.name,
|
||||
@@ -6,13 +6,10 @@ import {
|
||||
EVENT_DEFINITIONS_PER_PAGE,
|
||||
eventDefinitionsTableLogic,
|
||||
} from 'scenes/data-management/events/eventDefinitionsTableLogic'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
import { EventDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader'
|
||||
import { EventDefinitionProperties } from 'scenes/data-management/events/EventDefinitionProperties'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { LemonButton, LemonInput, LemonSelect, LemonSelectOptions, Link } from '@posthog/lemon-ui'
|
||||
import { More } from 'lib/lemon-ui/LemonButton/More'
|
||||
import { urls } from 'scenes/urls'
|
||||
@@ -35,12 +32,6 @@ const eventTypeOptions: LemonSelectOptions<EventDefinitionType> = [
|
||||
},
|
||||
]
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: EventDefinitionsTable,
|
||||
logic: eventDefinitionsTableLogic,
|
||||
paramsToProps: () => ({ syncWithUrl: true }),
|
||||
}
|
||||
|
||||
export function EventDefinitionsTable(): JSX.Element {
|
||||
const { eventDefinitions, eventDefinitionsLoading, filters } = useValues(eventDefinitionsTableLogic)
|
||||
const { loadEventDefinitions, setFilters } = useActions(eventDefinitionsTableLogic)
|
||||
@@ -124,14 +115,6 @@ export function EventDefinitionsTable(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div data-attr="manage-events-table">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
/>
|
||||
|
||||
<DataManagementPageTabs tab={DataManagementTab.EventDefinitions} />
|
||||
|
||||
<LemonBanner className="mb-4" type="info">
|
||||
Looking for{' '}
|
||||
{filters.event_type === 'event_custom'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea'
|
||||
import { AnyPropertyFilter, Breadcrumb, EventDefinitionType, EventDefinition, PropertyDefinition } from '~/types'
|
||||
import { AnyPropertyFilter, EventDefinitionType, EventDefinition, PropertyDefinition } from '~/types'
|
||||
import type { eventDefinitionsTableLogicType } from './eventDefinitionsTableLogicType'
|
||||
import api, { PaginatedResponse } from 'lib/api'
|
||||
import { keyMappingKeys } from 'lib/taxonomy'
|
||||
@@ -7,7 +7,6 @@ import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router'
|
||||
import { convertPropertyGroupToProperties, objectsEqual } from 'lib/utils'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
export interface EventDefinitionsPaginatedResponse extends PaginatedResponse<EventDefinition> {
|
||||
current?: string
|
||||
@@ -280,21 +279,6 @@ export const eventDefinitionsTableLogic = kea<eventDefinitionsTableLogicType>([
|
||||
selectors(({ cache }) => ({
|
||||
// Expose for testing
|
||||
apiCache: [() => [], () => cache.apiCache],
|
||||
breadcrumbs: [
|
||||
() => [],
|
||||
(): Breadcrumb[] => {
|
||||
return [
|
||||
{
|
||||
name: `Data Management`,
|
||||
path: urls.eventDefinitions(),
|
||||
},
|
||||
{
|
||||
name: 'Events',
|
||||
path: urls.eventDefinitions(),
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
})),
|
||||
listeners(({ actions, values, cache }) => ({
|
||||
setFilters: async () => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
|
||||
import { ActivityScope } from 'lib/components/ActivityLog/humanizeActivity'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
|
||||
export function History(): JSX.Element {
|
||||
return (
|
||||
<div data-attr="database-scene">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
/>
|
||||
<DataManagementPageTabs tab={DataManagementTab.History} />
|
||||
<ActivityLog
|
||||
scope={ActivityScope.DATA_MANAGEMENT}
|
||||
caption={
|
||||
'Only actions taken in the UI are captured in History. Automatic creation of definitions by ingestion is not shown here.'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: History,
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useValues } from 'kea'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { IngestionWarning, ingestionWarningsLogic, IngestionWarningSummary } from './ingestionWarningsLogic'
|
||||
import { LemonTable } from 'lib/lemon-ui/LemonTable'
|
||||
import { TZLabel } from 'lib/components/TZLabel'
|
||||
@@ -12,11 +9,6 @@ import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductI
|
||||
import { ProductKey } from '~/types'
|
||||
import { ReadingHog } from 'lib/components/hedgehogs'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: IngestionWarningsView,
|
||||
logic: ingestionWarningsLogic,
|
||||
}
|
||||
|
||||
const WARNING_TYPE_TO_DESCRIPTION = {
|
||||
cannot_merge_already_identified: 'Refused to merge an already identified user',
|
||||
cannot_merge_with_illegal_distinct_id: 'Refused to merge with an illegal distinct id',
|
||||
@@ -147,12 +139,6 @@ export function IngestionWarningsView(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div data-attr="manage-events-table">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
/>
|
||||
<DataManagementPageTabs tab={DataManagementTab.IngestionWarnings} />
|
||||
{data.length > 0 || dataLoading ? (
|
||||
<>
|
||||
<div className="mb-4">Data ingestion related warnings from past 30 days.</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { kea, connect, path, selectors, events } from 'kea'
|
||||
import { kea, connect, path, selectors, afterMount } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import { Breadcrumb } from '~/types'
|
||||
import { urls } from 'scenes/urls'
|
||||
@@ -82,10 +82,7 @@ export const ingestionWarningsLogic = kea<ingestionWarningsLogicType>([
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
events(({ actions }) => ({
|
||||
afterMount: () => {
|
||||
actions.loadData()
|
||||
},
|
||||
})),
|
||||
afterMount(({ actions }) => {
|
||||
actions.loadData()
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -2,7 +2,6 @@ import './PropertyDefinitionsTable.scss'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable'
|
||||
import { PropertyDefinition } from '~/types'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
import { PropertyDefinitionHeader } from 'scenes/data-management/events/DefinitionHeader'
|
||||
@@ -10,18 +9,10 @@ import {
|
||||
EVENT_PROPERTY_DEFINITIONS_PER_PAGE,
|
||||
propertyDefinitionsTableLogic,
|
||||
} from 'scenes/data-management/properties/propertyDefinitionsTableLogic'
|
||||
import { DataManagementPageTabs, DataManagementTab } from 'scenes/data-management/DataManagementPageTabs'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { LemonInput, LemonSelect, LemonTag, Link } from '@posthog/lemon-ui'
|
||||
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: PropertyDefinitionsTable,
|
||||
logic: propertyDefinitionsTableLogic,
|
||||
paramsToProps: () => ({ syncWithUrl: true }),
|
||||
}
|
||||
|
||||
export function PropertyDefinitionsTable(): JSX.Element {
|
||||
const { propertyDefinitions, propertyDefinitionsLoading, filters, propertyTypeOptions } =
|
||||
useValues(propertyDefinitionsTableLogic)
|
||||
@@ -73,12 +64,6 @@ export function PropertyDefinitionsTable(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div data-attr="manage-events-table">
|
||||
<PageHeader
|
||||
title="Data Management"
|
||||
caption="Use data management to organize events that come into PostHog. Reduce noise, clarify usage, and help collaborators get the most value from your data."
|
||||
tabbedPage
|
||||
/>
|
||||
<DataManagementPageTabs tab={DataManagementTab.PropertyDefinitions} />
|
||||
<LemonBanner className="mb-4" type="info">
|
||||
Looking for {filters.type === 'person' ? 'person ' : ''}property usage statistics?{' '}
|
||||
<Link
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
|
||||
import { Breadcrumb, PropertyDefinition } from '~/types'
|
||||
import { PropertyDefinition } from '~/types'
|
||||
import api from 'lib/api'
|
||||
import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router'
|
||||
import {
|
||||
@@ -82,21 +82,6 @@ export const propertyDefinitionsTableLogic = kea<propertyDefinitionsTableLogicTy
|
||||
],
|
||||
}),
|
||||
selectors({
|
||||
breadcrumbs: [
|
||||
() => [],
|
||||
(): Breadcrumb[] => {
|
||||
return [
|
||||
{
|
||||
name: `Data Management`,
|
||||
path: urls.eventDefinitions(),
|
||||
},
|
||||
{
|
||||
name: 'Properties',
|
||||
path: urls.propertyDefinitions(),
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
propertyTypeOptions: [
|
||||
(s) => [s.groupTypes, s.aggregationLabel],
|
||||
(groupTypes, aggregationLabel) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import api from 'lib/api'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { AnyPropertyFilter, Breadcrumb, DataWarehouseTable } from '~/types'
|
||||
import { DataTableNode } from '~/queries/schema'
|
||||
import { databaseSceneLogic } from 'scenes/data-management/database/databaseSceneLogic'
|
||||
import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic'
|
||||
import type { dataWarehouseTableLogicType } from './dataWarehouseTableLogicType'
|
||||
import { dataWarehouseSceneLogic } from '../external/dataWarehouseSceneLogic'
|
||||
|
||||
@@ -32,7 +32,7 @@ export const dataWarehouseTableLogic = kea<dataWarehouseTableLogicType>([
|
||||
props({} as TableLogicProps),
|
||||
connect(() => ({
|
||||
actions: [
|
||||
databaseSceneLogic,
|
||||
databaseTableListLogic,
|
||||
['loadDatabase'],
|
||||
dataWarehouseSceneLogic,
|
||||
['loadDataWarehouse', 'toggleSourceModal'],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemonButton, LemonTag, Link } from '@posthog/lemon-ui'
|
||||
import { PageHeader } from 'lib/components/PageHeader'
|
||||
import { SceneExport } from 'scenes/sceneTypes'
|
||||
import { databaseSceneLogic } from 'scenes/data-management/database/databaseSceneLogic'
|
||||
import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic'
|
||||
import { DataWarehousePageTabs, DataWarehouseTab } from '../DataWarehousePageTabs'
|
||||
import { DatabaseTablesContainer } from 'scenes/data-management/database/DatabaseTables'
|
||||
import { ViewLinkModal } from '../ViewLinkModal'
|
||||
@@ -12,7 +12,7 @@ import { FEATURE_FLAGS } from 'lib/constants'
|
||||
|
||||
export const scene: SceneExport = {
|
||||
component: DataWarehousePosthogScene,
|
||||
logic: databaseSceneLogic,
|
||||
logic: databaseTableListLogic,
|
||||
}
|
||||
|
||||
export function DataWarehousePosthogScene(): JSX.Element {
|
||||
|
||||