Skip to content

Commit

Permalink
feat: pre-fill existing tags for insights, actions, event definitions…
Browse files Browse the repository at this point in the history
…, and property definitions (#12544)

* pre-fill actions tags

* pre-fill insights tags

* add event and event property definitions tags APIs and load them in UI when editing

* extend the definitionEditLogic tests a little

* add a tags endpoint

* remove per item tag api endpoints

* remove more

* start moving to tagsModel

* wiring parts together

* update tags when action is saved

* tag refresh when editing definitions
  • Loading branch information
pauldambra authored Nov 24, 2022
1 parent fc21e8b commit 0013919
Show file tree
Hide file tree
Showing 17 changed files with 135 additions and 28 deletions.
21 changes: 20 additions & 1 deletion ee/api/test/test_tagged_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils import timezone
from rest_framework import status

from posthog.models import Dashboard, Tag
from posthog.models import Dashboard, Insight, Tag
from posthog.models.tagged_item import TaggedItem
from posthog.test.base import APIBaseTest

Expand Down Expand Up @@ -103,3 +103,22 @@ def test_no_duplicate_tags(self):
)

self.assertListEqual(sorted(response.json()["tags"]), ["a", "b"])

def test_can_list_tags(self) -> None:
from ee.models.license import License, LicenseManager

super(LicenseManager, cast(LicenseManager, License.objects)).create(
key="key_123", plan="enterprise", valid_until=timezone.datetime(2038, 1, 19, 3, 14, 7)
)

dashboard = Dashboard.objects.create(team_id=self.team.id, name="private dashboard")
tag = Tag.objects.create(name="dashboard tag", team_id=self.team.id)
dashboard.tagged_items.create(tag_id=tag.id)

insight = Insight.objects.create(team_id=self.team.id, name="empty insight")
tag = Tag.objects.create(name="insight tag", team_id=self.team.id)
insight.tagged_items.create(tag_id=tag.id)

response = self.client.get(f"/api/projects/{self.team.id}/tags")
assert response.status_code == status.HTTP_200_OK
assert response.json() == ["dashboard tag", "insight tag"]
10 changes: 10 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ class ApiRequest {
return this.events(teamId).addPathComponent(id)
}

public tags(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('tags')
}

// # Data management
public eventDefinitions(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('event_definitions')
Expand Down Expand Up @@ -567,6 +571,12 @@ const api = {
},
},

tags: {
async list(teamId: TeamType['id'] = getCurrentTeamId()): Promise<string[]> {
return new ApiRequest().tags(teamId).get()
},
},

eventDefinitions: {
async get({ eventDefinitionId }: { eventDefinitionId: EventDefinition['id'] }): Promise<EventDefinition> {
return new ApiRequest().eventDefinitionDetail(eventDefinitionId).get()
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/models/dashboardsModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { DashboardType, InsightShortId, DashboardTile, InsightModel } from '~/ty
import { urls } from 'scenes/urls'
import { teamLogic } from 'scenes/teamLogic'
import { lemonToast } from 'lib/components/lemonToast'
import { tagsModel } from '~/models/tagsModel'

export const dashboardsModel = kea<dashboardsModelType>({
path: ['models', 'dashboardsModel'],
connect: {
actions: [tagsModel, ['loadTags']],
},
actions: () => ({
delayedDeleteDashboard: (id: number) => ({ id }),
setDiveSourceId: (id: InsightShortId | null) => ({ id }),
Expand Down Expand Up @@ -119,6 +123,9 @@ export const dashboardsModel = kea<dashboardsModelType>({
values.rawDashboards[id]?.[updatedAttribute]?.length || 0,
payload[updatedAttribute].length
)
if (updatedAttribute === 'tags') {
actions.loadTags()
}
}
if (allowUndo) {
lemonToast.success('Dashboard updated', {
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/models/tagsModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { afterMount, kea, path } from 'kea'
import api from 'lib/api'

import type { tagsModelType } from './tagsModelType'
import { loaders } from 'kea-loaders'

export const tagsModel = kea<tagsModelType>([
path(['models', 'tagsModel']),
loaders(() => ({
tags: {
__default: [] as string[],
loadTags: async () => {
return (await api.tags.list()) || []
},
},
})),
afterMount(({ actions }) => actions.loadTags()),
])
3 changes: 3 additions & 0 deletions frontend/src/scenes/actions/ActionEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { LemonInput } from 'lib/components/LemonInput/LemonInput'
import { Form } from 'kea-forms'
import { LemonLabel } from 'lib/components/LemonLabel/LemonLabel'
import { IconPlayCircle } from 'lib/components/icons'
import { tagsModel } from '~/models/tagsModel'

export function ActionEdit({ action: loadedAction, id, onSave, temporaryToken }: ActionEditLogicProps): JSX.Element {
const logicProps: ActionEditLogicProps = {
Expand All @@ -34,6 +35,7 @@ export function ActionEdit({ action: loadedAction, id, onSave, temporaryToken }:
const { submitAction, deleteAction } = useActions(logic)
const { currentTeam } = useValues(teamLogic)
const { hasAvailableFeature } = useValues(userLogic)
const { tags } = useValues(tagsModel)

const slackEnabled = currentTeam?.slack_incoming_webhook

Expand Down Expand Up @@ -132,6 +134,7 @@ export function ActionEdit({ action: loadedAction, id, onSave, temporaryToken }:
onChange={(_, newTags) => onChange(newTags)}
className="action-tags"
saving={actionLoading}
tagsAvailable={tags.filter((tag) => !action.tags?.includes(tag))}
/>
)}
</Field>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/scenes/actions/actionEditLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { router } from 'kea-router'
import { urls } from 'scenes/urls'
import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic'
import { Link } from 'lib/components/Link'
import { tagsModel } from '~/models/tagsModel'

export type NewActionType = Partial<ActionType> &
Pick<ActionType, 'name' | 'post_to_slack' | 'slack_message_format' | 'steps'>
Expand All @@ -31,6 +32,7 @@ export const actionEditLogic = kea<actionEditLogicType>([
path(['scenes', 'actions', 'actionEditLogic']),
props({} as ActionEditLogicProps),
key((props) => props.id || 'new'),
connect({ actions: [tagsModel, ['loadTags']] }),
actions({
setAction: (action: Partial<ActionEditType>, options: SetActionProps = { merge: true }) => ({
action,
Expand Down Expand Up @@ -117,6 +119,7 @@ export const actionEditLogic = kea<actionEditLogicType>([
// reload actions so they are immediately available throughout the app
actions.loadEventDefinitions(null)
actions.loadActions()
actions.loadTags() // reload tags in case new tags are being saved
return action
},
},
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/scenes/dashboard/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { DashboardEventSource } from 'lib/utils/eventUsageLogic'
import { dashboardsModel } from '~/models/dashboardsModel'
import { AvailableFeature, DashboardMode, DashboardType, ExporterFormat } from '~/types'
import { dashboardLogic } from './dashboardLogic'
import { dashboardsLogic } from './dashboardsLogic'
import { DASHBOARD_RESTRICTION_OPTIONS } from './DashboardCollaborators'
import { userLogic } from 'scenes/userLogic'
import { privilegeLevelToName } from 'lib/constants'
Expand All @@ -30,6 +29,7 @@ import { DeleteDashboardModal } from 'scenes/dashboard/DeleteDashboardModal'
import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic'
import { DuplicateDashboardModal } from 'scenes/dashboard/DuplicateDashboardModal'
import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic'
import { tagsModel } from '~/models/tagsModel'

export function DashboardHeader(): JSX.Element | null {
const {
Expand All @@ -44,14 +44,15 @@ export function DashboardHeader(): JSX.Element | null {
textTileId,
} = useValues(dashboardLogic)
const { setDashboardMode, triggerDashboardUpdate } = useActions(dashboardLogic)
const { dashboardTags } = useValues(dashboardsLogic)
const { updateDashboard, pinDashboard, unpinDashboard } = useActions(dashboardsModel)

const { hasAvailableFeature } = useValues(userLogic)

const { showDuplicateDashboardModal } = useActions(duplicateDashboardLogic)
const { showDeleteDashboardModal } = useActions(deleteDashboardLogic)

const { tags } = useValues(tagsModel)

const { push } = useActions(router)

return dashboard || dashboardLoading ? (
Expand Down Expand Up @@ -320,7 +321,7 @@ export function DashboardHeader(): JSX.Element | null {
tags={dashboard.tags}
onChange={(_, tags) => triggerDashboardUpdate({ tags })}
saving={dashboardLoading}
tagsAvailable={dashboardTags.filter((tag) => !dashboard.tags?.includes(tag))}
tagsAvailable={tags.filter((tag) => !dashboard.tags?.includes(tag))}
className="insight-metadata-tags"
/>
) : dashboard.tags.length ? (
Expand Down
10 changes: 0 additions & 10 deletions frontend/src/scenes/dashboard/dashboardsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { kea } from 'kea'
import Fuse from 'fuse.js'
import { dashboardsModel } from '~/models/dashboardsModel'
import type { dashboardsLogicType } from './dashboardsLogicType'
import { DashboardType } from '~/types'
import { uniqueBy } from 'lib/utils'
import { userLogic } from 'scenes/userLogic'

export enum DashboardsTab {
Expand Down Expand Up @@ -61,13 +59,5 @@ export const dashboardsLogic = kea<dashboardsLogicType>({
.map((result) => result.item)
},
],
dashboardTags: [
() => [dashboardsModel.selectors.nameSortedDashboards],
(dashboards: DashboardType[]): string[] =>
uniqueBy(
dashboards.flatMap(({ tags }) => tags || ''),
(item) => item
).sort(),
],
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { isPostHogProp } from 'lib/components/PropertyKeyInfo'
import { VerifiedEventCheckbox } from 'lib/components/DefinitionPopup/DefinitionPopupContents'
import { LemonSelect } from 'lib/components/LemonSelect'
import { Form } from 'kea-forms'
import { tagsModel } from '~/models/tagsModel'

export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element {
const logic = definitionEditLogic(props)
const { definitionLoading, definition, hasTaxonomyFeatures, isEvent } = useValues(logic)
const { setPageMode, saveDefinition } = useActions(logic)
const { tags, tagsLoading } = useValues(tagsModel)

return (
<Form logic={definitionEditLogic} props={props} formKey="definition">
Expand Down Expand Up @@ -90,10 +92,11 @@ export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element {
{({ value, onChange }) => (
<ObjectTags
className="definition-tags"
saving={definitionLoading}
saving={definitionLoading || tagsLoading}
tags={value || []}
onChange={(_, tags) => onChange(tags)}
style={{ marginBottom: 4 }}
tagsAvailable={tags}
/>
)}
</Field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { definitionEditLogicType } from './definitionEditLogicType'
import { capitalizeFirstLetter } from 'lib/utils'
import { eventDefinitionsTableLogic } from 'scenes/data-management/events/eventDefinitionsTableLogic'
import { eventPropertyDefinitionsTableLogic } from 'scenes/data-management/event-properties/eventPropertyDefinitionsTableLogic'
import { tagsModel } from '~/models/tagsModel'

export interface DefinitionEditLogicProps extends DefinitionLogicProps {
definition: Definition
Expand All @@ -32,6 +33,8 @@ export const definitionEditLogic = kea<definitionEditLogicType>([
['setLocalEventPropertyDefinition'],
eventDefinitionsTableLogic,
['setLocalEventDefinition'],
tagsModel,
['loadTags'],
],
})),
forms(({ actions, props }) => ({
Expand Down Expand Up @@ -87,6 +90,7 @@ export const definitionEditLogic = kea<definitionEditLogicType>([
}
actions.setPageMode(DefinitionPageMode.View)
actions.setDefinition(definition)
actions.loadTags() // reload tags in case new tags are being saved
return definition
},
},
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/scenes/insights/Insight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/User
import clsx from 'clsx'
import { SharingModal } from 'lib/components/Sharing/SharingModal'
import { ExportButton } from 'lib/components/ExportButton/ExportButton'
import { tagsModel } from '~/models/tagsModel'

export function Insight({ insightId }: { insightId: InsightShortId | 'new' }): JSX.Element {
const { insightMode, subscriptionId } = useValues(insightSceneLogic)
Expand Down Expand Up @@ -67,6 +68,8 @@ export function Insight({ insightId }: { insightId: InsightShortId | 'new' }): J
const { cohortsById } = useValues(cohortsModel)
const { mathDefinitions } = useValues(mathsLogic)

const { tags } = useValues(tagsModel)

useEffect(() => {
reportInsightViewedForRecentInsights()
}, [insightId])
Expand Down Expand Up @@ -255,7 +258,7 @@ export function Insight({ insightId }: { insightId: InsightShortId | 'new' }): J
tags={insight.tags ?? []}
onChange={(_, tags) => setInsightMetadata({ tags: tags ?? [] })}
saving={tagLoading}
tagsAvailable={[]}
tagsAvailable={tags}
className="insight-metadata-tags"
data-attr="insight-tags"
/>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/scenes/insights/insightLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { insightsModel } from '~/models/insightsModel'
import { toLocalFilters } from './filters/ActionFilter/entityFilterLogic'
import { loaders } from 'kea-loaders'
import { legacyInsightQuery } from '~/queries/query'
import { tagsModel } from '~/models/tagsModel'

const IS_TEST_MODE = process.env.NODE_ENV === 'test'
const SHOW_TIMEOUT_MESSAGE_AFTER = 15000
Expand Down Expand Up @@ -104,6 +105,7 @@ export const insightLogic = kea<insightLogicType>([
mathsLogic,
['mathDefinitions'],
],
actions: [tagsModel, ['loadTags']],
logic: [eventUsageLogic, dashboardsModel, prompt({ key: `save-as-insight` })],
}),

Expand Down Expand Up @@ -263,6 +265,7 @@ export const insightLogic = kea<insightLogicType>([

savedInsightsLogic.findMounted()?.actions.loadInsights()
dashboardsModel.actions.updateDashboardInsight(updatedInsight)
actions.loadTags()

lemonToast.success(`Updated insight`, {
button: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const createInsight = (id: number, string = 'hi'): InsightModel =>
is_sample: false,
updated_at: 'now',
result: {},
tags: [],
color: null,
created_at: 'now',
dashboard: null,
Expand Down Expand Up @@ -183,6 +182,7 @@ describe('savedInsightsLogic', () => {
expect.objectContaining({ name: 'should be copied (copy)' })
)
})

it('can duplicate using name', async () => {
const sourceInsight = createInsight(123, 'hello')
sourceInsight.name = 'should be copied'
Expand Down
3 changes: 3 additions & 0 deletions posthog/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
property_definition,
sharing,
site_app,
tagged_item,
team,
uploaded_media,
user,
Expand Down Expand Up @@ -110,6 +111,8 @@ def api_not_found(request):

projects_router.register(r"uploaded_media", uploaded_media.MediaViewSet, "project_media", ["team_id"])

projects_router.register(r"tags", tagged_item.TaggedItemViewSet, "project_tags", ["team_id"])

# General endpoints (shared across CH & PG)
router.register(r"login", authentication.LoginViewSet)
router.register(r"login/precheck", authentication.LoginPrecheckViewSet)
Expand Down
Loading

0 comments on commit 0013919

Please sign in to comment.