Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pre-fill existing tags for insights, actions, event definitions, and property definitions #12544

Merged
merged 12 commits into from
Nov 24, 2022
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