chore: remove user groups (#34296)
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 148 KiB |
@@ -148,7 +148,6 @@ import {
|
||||
SurveyStatsResponse,
|
||||
TeamType,
|
||||
UserBasicType,
|
||||
UserGroup,
|
||||
UserInterviewType,
|
||||
UserType,
|
||||
} from '~/types'
|
||||
@@ -1080,23 +1079,6 @@ export class ApiRequest {
|
||||
return this.projectsDetail(teamId).addPathComponent('uploaded_media')
|
||||
}
|
||||
|
||||
// # UserGroups
|
||||
public userGroups(teamId?: TeamType['id']): ApiRequest {
|
||||
return this.projectsDetail(teamId).addPathComponent('user_groups')
|
||||
}
|
||||
|
||||
public userGroup(id: UserGroup['id']): ApiRequest {
|
||||
return this.userGroups().addPathComponent(id)
|
||||
}
|
||||
|
||||
public userGroupAddMember(id: UserGroup['id']): ApiRequest {
|
||||
return this.userGroup(id).addPathComponent('add')
|
||||
}
|
||||
|
||||
public userGroupRemoveMember(id: UserGroup['id']): ApiRequest {
|
||||
return this.userGroup(id).addPathComponent('remove')
|
||||
}
|
||||
|
||||
// # Alerts
|
||||
public alerts(alertId?: AlertType['id'], insightId?: InsightModel['id'], teamId?: TeamType['id']): ApiRequest {
|
||||
if (alertId) {
|
||||
@@ -2579,28 +2561,6 @@ const api = {
|
||||
},
|
||||
},
|
||||
|
||||
userGroups: {
|
||||
async list(): Promise<{ results: UserGroup[] }> {
|
||||
return await new ApiRequest().userGroups().get()
|
||||
},
|
||||
|
||||
async delete(id: UserGroup['id']): Promise<void> {
|
||||
return await new ApiRequest().userGroup(id).delete()
|
||||
},
|
||||
|
||||
async create(name: UserGroup['name']): Promise<UserGroup> {
|
||||
return await new ApiRequest().userGroups().create({ data: { name } })
|
||||
},
|
||||
|
||||
async addMember(id: UserGroup['id'], userId: UserBasicType['id']): Promise<UserGroup> {
|
||||
return await new ApiRequest().userGroupAddMember(id).create({ data: { userId } })
|
||||
},
|
||||
|
||||
async removeMember(id: UserGroup['id'], userId: UserBasicType['id']): Promise<UserGroup> {
|
||||
return await new ApiRequest().userGroupRemoveMember(id).create({ data: { userId } })
|
||||
},
|
||||
},
|
||||
|
||||
recordings: {
|
||||
async list(params: RecordingsQuery): Promise<RecordingsQueryResponse> {
|
||||
return await new ApiRequest().recordings().withQueryString(toParams(params)).get()
|
||||
|
||||
@@ -265,7 +265,6 @@ export const FEATURE_FLAGS = {
|
||||
USER_INTERVIEWS: 'user-interviews', // owner: @Twixes @jurajmajerik
|
||||
LOGS: 'logs', // owner: @david @frank @olly @ross
|
||||
CSP_REPORTING: 'mexicspo', // owner @pauldambra @lricoy @robbiec
|
||||
USER_GROUPS_ENABLED: 'user-groups-enabled', // owner: #team-error-tracking
|
||||
LLM_OBSERVABILITY_PLAYGROUND: 'llm-observability-playground', // owner: #team-llm-observability @peter-k
|
||||
USAGE_SPEND_DASHBOARDS: 'usage-spend-dashboards', // owner: @pawel-cebula #team-billing
|
||||
CDP_HOG_SOURCES: 'cdp-hog-sources', // owner #team-cdp
|
||||
|
||||
@@ -205,7 +205,6 @@ export const defaultMocks: Mocks = {
|
||||
'api/projects/:team_id/surveys': EMPTY_PAGINATED_RESPONSE,
|
||||
'api/projects/:team_id/surveys/responses_count': {},
|
||||
'api/environments/:team_id/integrations': EMPTY_PAGINATED_RESPONSE,
|
||||
'api/projects/:team_id/user_groups': EMPTY_PAGINATED_RESPONSE,
|
||||
'api/environments/:team_id/error_tracking/assignment_rules': EMPTY_PAGINATED_RESPONSE,
|
||||
'api/environments/:team_id/error_tracking/grouping_rules': EMPTY_PAGINATED_RESPONSE,
|
||||
'api/environments/:team_id/error_tracking/suppression_rules': EMPTY_PAGINATED_RESPONSE,
|
||||
|
||||
@@ -9071,7 +9071,7 @@
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": ["user_group", "user", "role"],
|
||||
"enum": ["user", "role"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2010,7 +2010,7 @@ export interface ErrorTrackingQuery extends DataNode<ErrorTrackingQueryResponse>
|
||||
}
|
||||
|
||||
export interface ErrorTrackingIssueAssignee {
|
||||
type: 'user_group' | 'user' | 'role'
|
||||
type: 'user' | 'role'
|
||||
id: integer | string
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ import {
|
||||
WebSnippet,
|
||||
} from './environment/TeamSettings'
|
||||
import { ProjectAccountFiltersSetting } from './environment/TestAccountFiltersConfig'
|
||||
import { UserGroups } from './environment/UserGroups'
|
||||
import { WebhookIntegration } from './environment/WebhookIntegration'
|
||||
import { Invites } from './organization/Invites'
|
||||
import { Members } from './organization/Members'
|
||||
@@ -407,13 +406,6 @@ export const SETTINGS_MAP: SettingSection[] = [
|
||||
title: 'Exception autocapture',
|
||||
component: <ExceptionAutocaptureSettings />,
|
||||
},
|
||||
{
|
||||
id: 'error-tracking-user-groups',
|
||||
title: 'User groups',
|
||||
description: 'Allow collections of users to be assigned to issues',
|
||||
component: <UserGroups />,
|
||||
flag: 'USER_GROUPS_ENABLED',
|
||||
},
|
||||
{
|
||||
id: 'error-tracking-alerting',
|
||||
title: 'Alerting',
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import { IconEllipsis, IconMinus } from '@posthog/icons'
|
||||
import {
|
||||
LemonButton,
|
||||
LemonMenu,
|
||||
LemonTable,
|
||||
LemonTableColumns,
|
||||
ProfileBubbles,
|
||||
ProfilePicture,
|
||||
} from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { MemberSelect } from 'lib/components/MemberSelect'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { UserBasicType, UserGroup } from '~/types'
|
||||
|
||||
import { userGroupsLogic } from './userGroupsLogic'
|
||||
|
||||
export const UserGroups = (): JSX.Element => {
|
||||
const { userGroups, userGroupsLoading } = useValues(userGroupsLogic)
|
||||
const { ensureAllGroupsLoaded, openGroupCreationForm } = useActions(userGroupsLogic)
|
||||
|
||||
useEffect(() => {
|
||||
ensureAllGroupsLoaded()
|
||||
}, [ensureAllGroupsLoaded])
|
||||
|
||||
const columns: LemonTableColumns<UserGroup> = [
|
||||
{
|
||||
title: 'Group',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 0,
|
||||
className: 'whitespace-nowrap font-semibold',
|
||||
},
|
||||
{
|
||||
title: 'Members',
|
||||
key: 'members',
|
||||
dataIndex: 'members',
|
||||
render: (_, { members }) => {
|
||||
return members && members.length ? (
|
||||
<ProfileBubbles
|
||||
people={members.map((user) => ({
|
||||
email: user.email,
|
||||
name: user.first_name,
|
||||
title: `${user.first_name} <${user.email}>`,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
'No members'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
render: (_, item: UserGroup) => <Actions group={item} />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="deprecated-space-y-2">
|
||||
<LemonTable
|
||||
size="small"
|
||||
dataSource={userGroups}
|
||||
loading={userGroupsLoading}
|
||||
columns={columns}
|
||||
expandable={{
|
||||
noIndent: true,
|
||||
rowExpandable: (record) => record.members.length > 0,
|
||||
expandedRowRender: (record) => <MembersTable groupId={record.id} members={record.members} />,
|
||||
}}
|
||||
/>
|
||||
<LemonButton onClick={openGroupCreationForm} size="small" type="primary">
|
||||
Create group
|
||||
</LemonButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Actions = ({ group }: { group: UserGroup }): JSX.Element => {
|
||||
const { addMember, deleteUserGroup } = useActions(userGroupsLogic)
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-end deprecated-space-x-2">
|
||||
<LemonMenu
|
||||
items={[
|
||||
{
|
||||
label: 'Delete',
|
||||
status: 'danger',
|
||||
onClick: () => deleteUserGroup(group.id),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LemonButton icon={<IconEllipsis />} size="xsmall" />
|
||||
</LemonMenu>
|
||||
<MemberSelect
|
||||
excludedMembers={group.members.map((m) => m.id)}
|
||||
onChange={(user) => {
|
||||
if (user) {
|
||||
addMember({ id: group.id, user: user })
|
||||
}
|
||||
}}
|
||||
value={null}
|
||||
allowNone={false}
|
||||
defaultLabel="Add member"
|
||||
type="primary"
|
||||
size="xsmall"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MembersTable = ({ groupId, members }: { groupId: UserGroup['id']; members: UserBasicType[] }): JSX.Element => {
|
||||
const { removeMember } = useActions(userGroupsLogic)
|
||||
|
||||
const columns: LemonTableColumns<UserBasicType> = [
|
||||
{
|
||||
title: 'Members',
|
||||
key: 'name',
|
||||
render: function ProfilePictureRender(_, member) {
|
||||
return <ProfilePicture user={member} showName />
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
render: (_, item: UserBasicType) => {
|
||||
return (
|
||||
<div className="flex flex-row justify-end">
|
||||
<LemonButton
|
||||
onClick={() => removeMember({ id: groupId, user: item })}
|
||||
icon={<IconMinus />}
|
||||
type="secondary"
|
||||
status="danger"
|
||||
size="xsmall"
|
||||
>
|
||||
Remove
|
||||
</LemonButton>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return <LemonTable size="small" showHeader={false} dataSource={members} columns={columns} embedded />
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { LemonDialog, LemonInput } from '@posthog/lemon-ui'
|
||||
import Fuse from 'fuse.js'
|
||||
import { actions, kea, listeners, path, reducers, selectors } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import api from 'lib/api'
|
||||
import { LemonField } from 'lib/lemon-ui/LemonField'
|
||||
|
||||
import { UserBasicType, UserGroup } from '~/types'
|
||||
|
||||
import type { userGroupsLogicType } from './userGroupsLogicType'
|
||||
|
||||
export interface UserGroupsFuse extends Fuse<UserGroup> {}
|
||||
|
||||
export const userGroupsLogic = kea<userGroupsLogicType>([
|
||||
path(['scenes', 'settings', 'environment', 'userGroupsLogic']),
|
||||
|
||||
actions({
|
||||
ensureAllGroupsLoaded: true,
|
||||
openGroupCreationForm: true,
|
||||
setSearch: (search) => ({ search }),
|
||||
}),
|
||||
|
||||
reducers({
|
||||
search: ['', { setSearch: (_, { search }) => search }],
|
||||
}),
|
||||
|
||||
loaders(({ values }) => ({
|
||||
userGroups: [
|
||||
[] as UserGroup[],
|
||||
{
|
||||
loadUserGroups: async () => {
|
||||
const response = await api.userGroups.list()
|
||||
return response.results
|
||||
},
|
||||
deleteUserGroup: async (id: string) => {
|
||||
await api.userGroups.delete(id)
|
||||
const newValues = [...values.userGroups]
|
||||
return newValues.filter((v) => v.id !== id)
|
||||
},
|
||||
createUserGroup: async (name: string) => {
|
||||
const response = await api.userGroups.create(name)
|
||||
return [...values.userGroups, response]
|
||||
},
|
||||
addMember: async ({ id, user }: { id: string; user: UserBasicType }) => {
|
||||
const group = values.userGroups.find((g) => g.id === id)
|
||||
if (group) {
|
||||
await api.userGroups.addMember(id, user.id)
|
||||
group.members = [...group.members, user]
|
||||
return values.userGroups.map((g) => (g.id === id ? group : g))
|
||||
}
|
||||
return values.userGroups
|
||||
},
|
||||
removeMember: async ({ id, user }: { id: string; user: UserBasicType }) => {
|
||||
const group = values.userGroups.find((g) => g.id === id)
|
||||
if (group) {
|
||||
await api.userGroups.removeMember(id, user.id)
|
||||
group.members = group.members.filter((m) => m.id !== user.id)
|
||||
return values.userGroups.map((g) => (g.id === id ? group : g))
|
||||
}
|
||||
return values.userGroups
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
||||
selectors({
|
||||
userGroupsFuse: [
|
||||
(s) => [s.userGroups],
|
||||
(userGroups): UserGroupsFuse => new Fuse<UserGroup>(userGroups, { keys: ['name'], threshold: 0.3 }),
|
||||
],
|
||||
filteredGroups: [
|
||||
(s) => [s.userGroups, s.userGroupsFuse, s.search],
|
||||
(userGroups, userGroupsFuse, search): UserGroup[] =>
|
||||
search ? userGroupsFuse.search(search).map((result) => result.item) : userGroups ?? [],
|
||||
],
|
||||
}),
|
||||
|
||||
listeners(({ values, actions }) => ({
|
||||
openGroupCreationForm: () => {
|
||||
LemonDialog.openForm({
|
||||
title: 'Create user group',
|
||||
initialValues: { name: '' },
|
||||
content: (
|
||||
<LemonField name="name">
|
||||
<LemonInput placeholder="Name" autoFocus />
|
||||
</LemonField>
|
||||
),
|
||||
errors: { name: (name) => (!name ? 'You must enter a name' : undefined) },
|
||||
onSubmit: ({ name }) => actions.createUserGroup(name),
|
||||
})
|
||||
},
|
||||
|
||||
ensureAllGroupsLoaded: () => {
|
||||
if (values.userGroupsLoading) {
|
||||
return
|
||||
}
|
||||
if (values.userGroups.length === 0) {
|
||||
actions.loadUserGroups()
|
||||
}
|
||||
},
|
||||
})),
|
||||
])
|
||||
@@ -5499,12 +5499,6 @@ export enum UserRole {
|
||||
Other = 'other',
|
||||
}
|
||||
|
||||
export type UserGroup = {
|
||||
id: string
|
||||
name: string
|
||||
members: UserBasicType[]
|
||||
}
|
||||
|
||||
export interface CoreMemory {
|
||||
id: string
|
||||
text: string
|
||||
|
||||
@@ -75,7 +75,6 @@ from . import (
|
||||
team,
|
||||
uploaded_media,
|
||||
user,
|
||||
user_group,
|
||||
external_web_analytics,
|
||||
web_vitals,
|
||||
)
|
||||
@@ -637,13 +636,6 @@ environments_router.register(
|
||||
["team_id"],
|
||||
)
|
||||
|
||||
projects_router.register(
|
||||
r"user_groups",
|
||||
user_group.UserGroupViewSet,
|
||||
"project_user_groups",
|
||||
["team_id"],
|
||||
)
|
||||
|
||||
projects_router.register(
|
||||
r"comments",
|
||||
comments.CommentViewSet,
|
||||
|
||||
@@ -61,10 +61,10 @@ class ErrorTrackingIssueAssignmentSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "type"]
|
||||
|
||||
def get_id(self, obj):
|
||||
return obj.user_id or obj.user_group_id or obj.role_id
|
||||
return obj.user_id or obj.role_id
|
||||
|
||||
def get_type(self, obj):
|
||||
return "user_group" if obj.user_group else "role" if obj.role else "user"
|
||||
return "role" if obj.role else "user"
|
||||
|
||||
|
||||
class ErrorTrackingIssueSerializer(serializers.ModelSerializer):
|
||||
@@ -276,7 +276,6 @@ def assign_issue(issue: ErrorTrackingIssue, assignee, organization, user, team_i
|
||||
issue_id=issue.id,
|
||||
defaults={
|
||||
"user_id": None if assignee["type"] != "user" else assignee["id"],
|
||||
"user_group_id": None if assignee["type"] != "user_group" else assignee["id"],
|
||||
"role_id": None if assignee["type"] != "role" else assignee["id"],
|
||||
},
|
||||
)
|
||||
@@ -574,8 +573,6 @@ class ErrorTrackingAssignmentRuleSerializer(serializers.ModelSerializer):
|
||||
def get_assignee(self, obj):
|
||||
if obj.user_id:
|
||||
return {"type": "user", "id": obj.user_id}
|
||||
elif obj.user_group_id:
|
||||
return {"type": "user_group", "id": obj.user_group_id}
|
||||
elif obj.role_id:
|
||||
return {"type": "role", "id": obj.role_id}
|
||||
return None
|
||||
@@ -601,7 +598,6 @@ class ErrorTrackingAssignmentRuleViewSet(TeamAndOrgViewSetMixin, viewsets.ModelV
|
||||
|
||||
if assignee:
|
||||
assignment_rule.user_id = None if assignee["type"] != "user" else assignee["id"]
|
||||
assignment_rule.user_group_id = None if assignee["type"] != "user_group" else assignee["id"]
|
||||
assignment_rule.role_id = None if assignee["type"] != "role" else assignee["id"]
|
||||
|
||||
assignment_rule.save()
|
||||
@@ -627,7 +623,6 @@ class ErrorTrackingAssignmentRuleViewSet(TeamAndOrgViewSetMixin, viewsets.ModelV
|
||||
bytecode=bytecode,
|
||||
order_key=0,
|
||||
user_id=None if assignee["type"] != "user" else assignee["id"],
|
||||
user_group_id=None if assignee["type"] != "user_group" else assignee["id"],
|
||||
role_id=None if assignee["type"] != "role" else assignee["id"],
|
||||
)
|
||||
|
||||
@@ -646,8 +641,6 @@ class ErrorTrackingGroupingRuleSerializer(serializers.ModelSerializer):
|
||||
def get_assignee(self, obj):
|
||||
if obj.user_id:
|
||||
return {"type": "user", "id": obj.user_id}
|
||||
elif obj.user_group_id:
|
||||
return {"type": "user_group", "id": obj.user_group_id}
|
||||
elif obj.role_id:
|
||||
return {"type": "role", "id": obj.role_id}
|
||||
return None
|
||||
@@ -674,7 +667,6 @@ class ErrorTrackingGroupingRuleViewSet(TeamAndOrgViewSetMixin, viewsets.ModelVie
|
||||
|
||||
if assignee:
|
||||
grouping_rule.user_id = None if assignee["type"] != "user" else assignee["id"]
|
||||
grouping_rule.user_group_id = None if assignee["type"] != "user_group" else assignee["id"]
|
||||
grouping_rule.role_id = None if assignee["type"] != "role" else assignee["id"]
|
||||
|
||||
if description:
|
||||
@@ -701,7 +693,6 @@ class ErrorTrackingGroupingRuleViewSet(TeamAndOrgViewSetMixin, viewsets.ModelVie
|
||||
bytecode=bytecode,
|
||||
order_key=0,
|
||||
user_id=None if (not assignee or assignee["type"] != "user") else assignee["id"],
|
||||
user_group_id=None if (not assignee or assignee["type"] != "user_group") else assignee["id"],
|
||||
role_id=None if (not assignee or assignee["type"] != "role") else assignee["id"],
|
||||
description=description,
|
||||
)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
from posthog.test.base import APIBaseTest
|
||||
from posthog.models import UserGroup, UserGroupMembership
|
||||
|
||||
|
||||
class TestUserGroup(APIBaseTest):
|
||||
def test_group_creation(self):
|
||||
self.client.post(
|
||||
f"/api/projects/{self.team.id}/user_groups",
|
||||
data={"name": "My team"},
|
||||
)
|
||||
user_group = UserGroup.objects.get(team=self.team)
|
||||
self.assertIsNotNone(user_group)
|
||||
self.assertEqual(user_group.name, "My team")
|
||||
|
||||
def test_add_group_members(self):
|
||||
user_group = UserGroup.objects.create(team=self.team)
|
||||
self.client.post(
|
||||
f"/api/projects/{self.team.id}/user_groups/{user_group.id}/add",
|
||||
data={"userId": self.user.id},
|
||||
)
|
||||
self.assertEqual(user_group.members.count(), 1)
|
||||
|
||||
def test_remove_group_members(self):
|
||||
user_group = UserGroup.objects.create(team=self.team)
|
||||
UserGroupMembership.objects.create(group=user_group, user=self.user)
|
||||
self.assertEqual(user_group.members.count(), 1)
|
||||
self.client.post(
|
||||
f"/api/projects/{self.team.id}/user_groups/{user_group.id}/remove",
|
||||
data={"userId": self.user.id},
|
||||
)
|
||||
self.assertEqual(user_group.members.count(), 0)
|
||||
@@ -1,42 +0,0 @@
|
||||
from rest_framework import serializers, viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from posthog.api.shared import UserBasicSerializer
|
||||
from posthog.api.routing import TeamAndOrgViewSetMixin
|
||||
from posthog.api.utils import action
|
||||
from posthog.models.user_group import UserGroup, UserGroupMembership
|
||||
|
||||
|
||||
class UserGroupSerializer(serializers.ModelSerializer):
|
||||
members = UserBasicSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = UserGroup
|
||||
fields = ["id", "name", "members"]
|
||||
|
||||
def create(self, validated_data: dict, *args, **kwargs) -> UserGroup:
|
||||
return UserGroup.objects.create(
|
||||
team=self.context["get_team"](),
|
||||
**validated_data,
|
||||
)
|
||||
|
||||
|
||||
class UserGroupViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
|
||||
scope_object = "INTERNAL"
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupSerializer
|
||||
|
||||
def safely_get_queryset(self, queryset):
|
||||
return queryset.filter(team=self.team)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def add(self, request, **kwargs):
|
||||
group = self.get_object()
|
||||
UserGroupMembership.objects.get_or_create(group=group, user_id=request.data["userId"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def remove(self, request, **kwargs):
|
||||
group = self.get_object()
|
||||
UserGroupMembership.objects.filter(group=group, user_id=request.data["userId"]).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1,42 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from posthog.models.user_group import UserGroup
|
||||
from ee.models.rbac.role import Role, RoleMembership
|
||||
from posthog.models.team import Team
|
||||
from posthog.models.error_tracking import (
|
||||
ErrorTrackingIssueAssignment,
|
||||
ErrorTrackingAssignmentRule,
|
||||
ErrorTrackingGroupingRule,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Migrate all user groups to be roles instead"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--team-id", default=None, type=int, help="Team ID to migrate")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
team_id = options["team_id"]
|
||||
|
||||
if team_id:
|
||||
team_ids = [team_id]
|
||||
else:
|
||||
team_ids = list(UserGroup.objects.values_list("team_id", flat=True).distinct())
|
||||
|
||||
for id in team_ids:
|
||||
team = Team.objects.get(id=id)
|
||||
user_groups = UserGroup.objects.filter(team=team)
|
||||
|
||||
for user_group in user_groups:
|
||||
# create roles for each user group
|
||||
(role, _) = Role.objects.get_or_create(organization=team.organization, name=user_group.name)
|
||||
|
||||
# create memberships for each user
|
||||
members = user_group.members.all()
|
||||
memberships = [RoleMembership(user=user, role=role) for user in members]
|
||||
RoleMembership.objects.bulk_create(memberships, ignore_conflicts=True)
|
||||
|
||||
# update references in error tracking models from user_group_id to role_id
|
||||
ErrorTrackingIssueAssignment.objects.filter(user_group=user_group).update(role=role, user_group=None)
|
||||
ErrorTrackingAssignmentRule.objects.filter(user_group=user_group).update(role=role, user_group=None)
|
||||
ErrorTrackingGroupingRule.objects.filter(user_group=user_group).update(role=role, user_group=None)
|
||||
@@ -1,73 +0,0 @@
|
||||
from posthog.models.user_group import UserGroup, UserGroupMembership
|
||||
from ee.models.rbac.role import Role, RoleMembership
|
||||
from django.core.management import call_command
|
||||
from posthog.test.base import BaseTest
|
||||
from posthog.models.error_tracking import (
|
||||
ErrorTrackingIssue,
|
||||
ErrorTrackingIssueAssignment,
|
||||
ErrorTrackingAssignmentRule,
|
||||
ErrorTrackingGroupingRule,
|
||||
)
|
||||
|
||||
|
||||
class TestMigrateUserGroupsToRoles(BaseTest):
|
||||
def test_migrates_group_and_members_to_roles(self):
|
||||
user_group = UserGroup.objects.create(team=self.team, name="Test group")
|
||||
UserGroupMembership.objects.create(group=user_group, user=self.user)
|
||||
|
||||
issue = ErrorTrackingIssue.objects.create(team=self.team)
|
||||
issue_assignment = ErrorTrackingIssueAssignment.objects.create(issue=issue, user_group=user_group)
|
||||
assignment_rule = ErrorTrackingAssignmentRule.objects.create(
|
||||
team=self.team, user_group=user_group, order_key=0, bytecode={}, filters={}
|
||||
)
|
||||
grouping_rule = ErrorTrackingGroupingRule.objects.create(
|
||||
team=self.team, user_group=user_group, order_key=0, bytecode={}, filters={}
|
||||
)
|
||||
|
||||
assert Role.objects.count() == 0
|
||||
|
||||
call_command(
|
||||
"migrate_user_groups_to_roles",
|
||||
f"--team-id={str(self.team.pk)}",
|
||||
)
|
||||
|
||||
# creates role
|
||||
assert Role.objects.count() == 1
|
||||
role = Role.objects.first()
|
||||
assert role is not None
|
||||
assert role.name == user_group.name
|
||||
|
||||
# updates member
|
||||
assert len(role.members.all()) == 1
|
||||
member = role.members.first()
|
||||
assert member is not None
|
||||
assert member == self.user
|
||||
|
||||
# update error tracking rules
|
||||
issue_assignment.refresh_from_db()
|
||||
assignment_rule.refresh_from_db()
|
||||
grouping_rule.refresh_from_db()
|
||||
|
||||
assert issue_assignment.user_group is None
|
||||
assert assignment_rule.user_group is None
|
||||
assert grouping_rule.user_group is None
|
||||
|
||||
assert issue_assignment.role == role
|
||||
assert assignment_rule.role == role
|
||||
assert grouping_rule.role == role
|
||||
|
||||
def test_role_of_same_name_exists(self):
|
||||
user_group = UserGroup.objects.create(team=self.team, name="Test group")
|
||||
role = Role.objects.create(organization=self.team.organization, name=user_group.name)
|
||||
RoleMembership.objects.create(role=role, user=self.user)
|
||||
|
||||
assert Role.objects.count() == 1
|
||||
assert RoleMembership.objects.count() == 1
|
||||
|
||||
call_command(
|
||||
"migrate_user_groups_to_roles",
|
||||
f"--team-id={str(self.team.pk)}",
|
||||
)
|
||||
|
||||
assert Role.objects.count() == 1
|
||||
assert RoleMembership.objects.count() == 1
|
||||
@@ -2,6 +2,7 @@ from django.db import models, transaction
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django_deprecate_fields import deprecate_field
|
||||
|
||||
from posthog.models.utils import UUIDModel
|
||||
from ee.models.rbac.role import Role
|
||||
@@ -86,7 +87,8 @@ class ErrorTrackingExternalReference(UUIDModel):
|
||||
class ErrorTrackingIssueAssignment(UUIDModel):
|
||||
issue = models.OneToOneField(ErrorTrackingIssue, on_delete=models.CASCADE, related_name="assignment")
|
||||
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||
user_group = models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE)
|
||||
# DEPRECATED: issues can only be assigned to users or roles
|
||||
user_group = deprecate_field(models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE))
|
||||
role = models.ForeignKey(Role, null=True, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -178,7 +180,8 @@ class ErrorTrackingSymbolSet(UUIDModel):
|
||||
class ErrorTrackingAssignmentRule(UUIDModel):
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||
user_group = models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE)
|
||||
# DEPRECATED: issues can only be assigned to users or roles
|
||||
user_group = deprecate_field(models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE))
|
||||
role = models.ForeignKey(Role, null=True, on_delete=models.CASCADE)
|
||||
order_key = models.IntegerField(null=False, blank=False)
|
||||
bytecode = models.JSONField(null=False, blank=False) # The bytecode of the rule
|
||||
@@ -224,7 +227,8 @@ class ErrorTrackingGroupingRule(UUIDModel):
|
||||
# in favour of the assignment of the grouping rule. Notably this differs from assignment rules
|
||||
# in so far as we permit all of these to be null
|
||||
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||
user_group = models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE)
|
||||
# DEPRECATED: issues can only be assigned to users or roles
|
||||
user_group = deprecate_field(models.ForeignKey(UserGroup, null=True, on_delete=models.CASCADE))
|
||||
role = models.ForeignKey(Role, null=True, on_delete=models.CASCADE)
|
||||
|
||||
# Users will probably find it convenient to be able to add a short description to grouping rules
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import pytest
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from posthog.test.base import BaseTest
|
||||
from posthog.models.user_group import UserGroup, UserGroupMembership
|
||||
|
||||
|
||||
class TestUserGroup(BaseTest):
|
||||
def test_user_group_membership_cascade_deletes(self):
|
||||
group = UserGroup.objects.create(team=self.team)
|
||||
UserGroupMembership.objects.create(group=group, user=self.user)
|
||||
|
||||
assert UserGroupMembership.objects.count() == 1
|
||||
group.delete()
|
||||
assert UserGroupMembership.objects.count() == 0
|
||||
|
||||
def test_user_group_membership_uniqueness(self):
|
||||
group = UserGroup.objects.create(team=self.team)
|
||||
with pytest.raises(IntegrityError):
|
||||
UserGroupMembership.objects.create(group=group, user=self.user)
|
||||
UserGroupMembership.objects.create(group=group, user=self.user)
|
||||
@@ -4,6 +4,10 @@ from posthog.models.team import Team
|
||||
from posthog.models.user import User
|
||||
|
||||
|
||||
# !!! DEPRECATED !!!
|
||||
# Please use the ee.Role model instead
|
||||
|
||||
|
||||
class UserGroup(UUIDModel):
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="user_groups")
|
||||
name = models.TextField()
|
||||
|
||||
@@ -958,7 +958,6 @@ class ErrorTrackingIssueAggregations(BaseModel):
|
||||
|
||||
|
||||
class Type2(StrEnum):
|
||||
USER_GROUP = "user_group"
|
||||
USER = "user"
|
||||
ROLE = "role"
|
||||
|
||||
|
||||
@@ -461,13 +461,6 @@ def send_error_tracking_issue_assigned(assignment: ErrorTrackingIssueAssignment,
|
||||
for membership in memberships_to_email
|
||||
if (membership.user == assignment.user and membership.user != assigner)
|
||||
]
|
||||
elif assignment.user_group:
|
||||
group_users = assignment.user_group.members.all()
|
||||
memberships_to_email = [
|
||||
membership
|
||||
for membership in memberships_to_email
|
||||
if (membership.user in group_users and membership.user != assigner)
|
||||
]
|
||||
elif assignment.role:
|
||||
role_users = assignment.role.members.all()
|
||||
memberships_to_email = [
|
||||
|
||||
@@ -63,11 +63,14 @@ export const GroupDisplays = ({ sizes }: SizedComponentProps): JSX.Element => {
|
||||
size={size}
|
||||
assignee={{
|
||||
id: '123',
|
||||
type: 'group',
|
||||
group: {
|
||||
type: 'role',
|
||||
role: {
|
||||
id: '123',
|
||||
name: 'Group Name',
|
||||
name: 'Role Name',
|
||||
feature_flags_access_level: 37,
|
||||
members: [],
|
||||
created_at: '2021-08-02 12:34:56',
|
||||
created_by: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -49,15 +49,6 @@ function getIconClassname(size: 'xsmall' | 'small' | 'medium' | 'large' = 'mediu
|
||||
|
||||
export const AssigneeIconDisplay = ({ assignee, size }: AssigneeIconDisplayProps): JSX.Element => {
|
||||
return match(assignee)
|
||||
.with({ type: 'group' }, ({ group }) => (
|
||||
// The ideal way would be to use a Lettermark component here
|
||||
// but there is no way to make it consistent with ProfilePicture at the moment
|
||||
// TODO: Make sure the size prop are the same between ProfilePicture and Lettermark
|
||||
<ProfilePicture
|
||||
user={{ first_name: group.name, last_name: undefined, email: undefined }}
|
||||
className={getIconClassname(size)}
|
||||
/>
|
||||
))
|
||||
.with({ type: 'role' }, ({ role }) => (
|
||||
// The ideal way would be to use a Lettermark component here
|
||||
// but there is no way to make it consistent with ProfilePicture at the moment
|
||||
@@ -99,7 +90,6 @@ export const AssigneeLabelDisplay = ({
|
||||
})}
|
||||
>
|
||||
{match(assignee)
|
||||
.with({ type: 'group' }, ({ group }) => group.name)
|
||||
.with({ type: 'role' }, ({ role }) => role.name)
|
||||
.with({ type: 'user' }, ({ user }) => fullName(user))
|
||||
.otherwise(() => placeholder || 'Unassigned')}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IconPlusSmall, IconX } from '@posthog/icons'
|
||||
import { LemonButton, LemonInput } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
import { ErrorTrackingIssue, ErrorTrackingIssueAssignee } from '~/queries/schema/schema-general'
|
||||
@@ -15,10 +14,8 @@ export interface AssigneeDropdownProps {
|
||||
}
|
||||
|
||||
export function AssigneeDropdown({ assignee, onChange }: AssigneeDropdownProps): JSX.Element {
|
||||
const { search, filteredGroups, filteredRoles, filteredMembers, userGroupsLoading, rolesLoading, membersLoading } =
|
||||
useValues(assigneeSelectLogic)
|
||||
const { search, filteredRoles, filteredMembers, rolesLoading, membersLoading } = useValues(assigneeSelectLogic)
|
||||
const { setSearch } = useActions(assigneeSelectLogic)
|
||||
const userGroupsEnabled = useFeatureFlag('USER_GROUPS_ENABLED')
|
||||
|
||||
return (
|
||||
<div className="max-w-100 deprecated-space-y-2 overflow-hidden">
|
||||
@@ -62,32 +59,6 @@ export function AssigneeDropdown({ assignee, onChange }: AssigneeDropdownProps):
|
||||
}
|
||||
/>
|
||||
|
||||
{userGroupsEnabled && (
|
||||
<Section
|
||||
title="Groups"
|
||||
loading={userGroupsLoading}
|
||||
search={!!search}
|
||||
type="user_group"
|
||||
items={filteredGroups.map((group) => ({
|
||||
id: group.id,
|
||||
type: 'group',
|
||||
group: group,
|
||||
}))}
|
||||
onSelect={onChange}
|
||||
activeId={assignee?.id}
|
||||
emptyState={
|
||||
<LemonButton
|
||||
fullWidth
|
||||
size="small"
|
||||
icon={<IconPlusSmall />}
|
||||
to={urls.settings('environment-error-tracking', 'user-groups')}
|
||||
>
|
||||
<div className="text-secondary">Create user group</div>
|
||||
</LemonButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Section
|
||||
title="Users"
|
||||
loading={membersLoading}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import Fuse from 'fuse.js'
|
||||
import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea'
|
||||
import { membersLogic } from 'scenes/organization/membersLogic'
|
||||
import { userGroupsLogic } from 'scenes/settings/environment/userGroupsLogic'
|
||||
import { rolesLogic } from 'scenes/settings/organization/Permissions/Roles/rolesLogic'
|
||||
|
||||
import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
|
||||
import type { OrganizationMemberType, RoleType, UserGroup } from '~/types'
|
||||
import type { OrganizationMemberType, RoleType } from '~/types'
|
||||
|
||||
import type { assigneeSelectLogicType } from './assigneeSelectLogicType'
|
||||
|
||||
@@ -19,19 +18,13 @@ export type UserAssignee = {
|
||||
user: OrganizationMemberType['user']
|
||||
}
|
||||
|
||||
export type GroupAssignee = {
|
||||
id: string
|
||||
type: 'group'
|
||||
group: UserGroup
|
||||
}
|
||||
|
||||
export type RoleAssignee = {
|
||||
id: string
|
||||
type: 'role'
|
||||
role: RoleType
|
||||
}
|
||||
|
||||
export type Assignee = UserAssignee | GroupAssignee | RoleAssignee | null
|
||||
export type Assignee = UserAssignee | RoleAssignee | null
|
||||
|
||||
export interface RolesFuse extends Fuse<RoleType> {}
|
||||
|
||||
@@ -43,17 +36,10 @@ export const assigneeSelectLogic = kea<assigneeSelectLogicType>([
|
||||
values: [
|
||||
membersLogic,
|
||||
['meFirstMembers', 'filteredMembers', 'membersLoading'],
|
||||
userGroupsLogic,
|
||||
['userGroups', 'filteredGroups', 'userGroupsLoading'],
|
||||
rolesLogic,
|
||||
['roles', 'rolesLoading'],
|
||||
],
|
||||
actions: [
|
||||
membersLogic,
|
||||
['setSearch as setMembersSearch', 'ensureAllMembersLoaded'],
|
||||
userGroupsLogic,
|
||||
['setSearch as setGroupsSearch', 'ensureAllGroupsLoaded'],
|
||||
],
|
||||
actions: [membersLogic, ['setSearch as setMembersSearch', 'ensureAllMembersLoaded']],
|
||||
})),
|
||||
|
||||
actions({
|
||||
@@ -67,20 +53,17 @@ export const assigneeSelectLogic = kea<assigneeSelectLogicType>([
|
||||
|
||||
listeners(({ values, actions }) => ({
|
||||
setSearch: () => {
|
||||
actions.setGroupsSearch(values.search)
|
||||
actions.setMembersSearch(values.search)
|
||||
},
|
||||
ensureAssigneeTypesLoaded: () => {
|
||||
actions.ensureAllGroupsLoaded()
|
||||
actions.ensureAllMembersLoaded()
|
||||
},
|
||||
})),
|
||||
|
||||
selectors({
|
||||
loading: [
|
||||
(s) => [s.membersLoading, s.userGroupsLoading, s.rolesLoading],
|
||||
(membersLoading, userGroupsLoading, rolesLoading): boolean =>
|
||||
membersLoading || userGroupsLoading || rolesLoading,
|
||||
(s) => [s.membersLoading, s.rolesLoading],
|
||||
(membersLoading, rolesLoading): boolean => membersLoading || rolesLoading,
|
||||
],
|
||||
|
||||
rolesFuse: [
|
||||
@@ -94,20 +77,11 @@ export const assigneeSelectLogic = kea<assigneeSelectLogicType>([
|
||||
],
|
||||
|
||||
resolveAssignee: [
|
||||
(s) => [s.userGroups, s.roles, s.meFirstMembers],
|
||||
(groups, roles, members): ((assignee: ErrorTrackingIssue['assignee']) => Assignee) => {
|
||||
(s) => [s.roles, s.meFirstMembers],
|
||||
(roles, members): ((assignee: ErrorTrackingIssue['assignee']) => Assignee) => {
|
||||
return (assignee: ErrorTrackingIssue['assignee']) => {
|
||||
if (assignee) {
|
||||
if (assignee.type === 'user_group') {
|
||||
const assignedGroup = groups.find((group) => group.id === assignee.id)
|
||||
return assignedGroup
|
||||
? {
|
||||
id: assignedGroup.id,
|
||||
type: 'group',
|
||||
group: assignedGroup,
|
||||
}
|
||||
: null
|
||||
} else if (assignee.type === 'role') {
|
||||
if (assignee.type === 'role') {
|
||||
const assignedRole = roles.find((role) => role.id === assignee.id)
|
||||
return assignedRole
|
||||
? {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, issue_id, user_id, user_group_id, role_id, created_at FROM posthog_errortrackingissueassignment\n WHERE issue_id = $1\n ",
|
||||
"query": "\n SELECT id, issue_id, user_id, role_id, created_at FROM posthog_errortrackingissueassignment\n WHERE issue_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,16 +20,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "user_group_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "role_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -44,9 +39,8 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cbbb85032e24157ba28500031432cd7344c486957d39f6fa47b4dad80ae0f626"
|
||||
"hash": "1b96d742b758d18f8722dbcec8783657b78776f03bdaf32104ce7d78cc762790"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO posthog_errortrackingissueassignment (id, issue_id, user_id, user_group_id, role_id, created_at)\n VALUES ($1, $2, $3, $4, $5, NOW())\n ON CONFLICT (issue_id) DO UPDATE SET issue_id = $2 -- no-op to get a returned row\n RETURNING id, issue_id, user_id, user_group_id, role_id, created_at\n ",
|
||||
"query": "\n INSERT INTO posthog_errortrackingissueassignment (id, issue_id, user_id, role_id, created_at)\n VALUES ($1, $2, $3, $4, NOW())\n ON CONFLICT (issue_id) DO UPDATE SET issue_id = $2 -- no-op to get a returned row\n RETURNING id, issue_id, user_id, role_id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,16 +20,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "user_group_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "role_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -39,7 +34,6 @@
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
@@ -48,9 +42,8 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "caf8d15152f706524e4d7e55e9b64fc604838f0b1054c8f3b5a39751ecf5014e"
|
||||
"hash": "40d9604a9c7d7f3e3fc71211ac44dff2413bc92a43ae9d3c7f6ae92c5a386f18"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, team_id, user_id, user_group_id, role_id, order_key, bytecode, created_at, updated_at\n FROM posthog_errortrackinggroupingrule\n WHERE team_id = $1 AND disabled_data IS NULL\n ",
|
||||
"query": "\n SELECT id, team_id, user_id, role_id, order_key, bytecode, created_at, updated_at\n FROM posthog_errortrackingassignmentrule\n WHERE team_id = $1 AND disabled_data IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,31 +20,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "user_group_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "role_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "order_key",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 5,
|
||||
"name": "bytecode",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 7,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -59,12 +54,11 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f8bced8a6b1b63e7311bbd03bf992ac2371808f73d0bc8744282a271f3f2b131"
|
||||
"hash": "47f0acddabe1cdf727b834a26367a441ed1b740b73c1189692c715c59826ddf3"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, team_id, user_id, user_group_id, role_id, order_key, bytecode, created_at, updated_at\n FROM posthog_errortrackingassignmentrule\n WHERE team_id = $1 AND disabled_data IS NULL\n ",
|
||||
"query": "\n SELECT id, team_id, user_id, role_id, order_key, bytecode, created_at, updated_at\n FROM posthog_errortrackinggroupingrule\n WHERE team_id = $1 AND disabled_data IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,31 +20,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "user_group_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "role_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "order_key",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 5,
|
||||
"name": "bytecode",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 7,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -59,12 +54,11 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "51eb1534432ea185cf0357375a7e09fd2bb76f3937ce4773a6bda824a8bcf8a3"
|
||||
"hash": "575b8f4c016f9c61eefc2307ec96840aac8134ea6fd2a78b9a999efde05f6f4d"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO posthog_eventproperty (event, property, team_id, project_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9e0e25b9966a23792427c27a80888a75efdb8abe195339e0a1676ebed6fc61ef"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO posthog_propertydefinition (id, name, type, group_type_index, is_numerical, volume_30_day, query_usage_30_day, team_id, project_id, property_type)\n VALUES ($1, $2, $3, $4, $5, NULL, NULL, $6, $7, $8)\n ON CONFLICT (coalesce(project_id, team_id::bigint), name, type, coalesce(group_type_index, -1))\n DO UPDATE SET property_type=EXCLUDED.property_type WHERE posthog_propertydefinition.property_type IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Int2",
|
||||
"Int2",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b954e5f88405dd706b709c313738872f63a186a0a2f64f1f2c4b2a61dd434fce"
|
||||
}
|
||||
@@ -19,26 +19,17 @@ use crate::{error::UnhandledError, issue_resolution::Issue, types::OutputErrProp
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewAssignment {
|
||||
pub user_id: Option<i32>,
|
||||
pub user_group_id: Option<Uuid>,
|
||||
pub role_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl NewAssignment {
|
||||
// Returns None if this cannot be used to construct a valid assignment, ensuring all
|
||||
// NewAssignments have at least one of user_id, user_group_id, or role_id set.
|
||||
pub fn try_new(
|
||||
user_id: Option<i32>,
|
||||
user_group_id: Option<Uuid>,
|
||||
role_id: Option<Uuid>,
|
||||
) -> Option<Self> {
|
||||
if user_id.is_none() && user_group_id.is_none() && role_id.is_none() {
|
||||
// NewAssignments have at least one of user_id or role_id set.
|
||||
pub fn try_new(user_id: Option<i32>, role_id: Option<Uuid>) -> Option<Self> {
|
||||
if user_id.is_none() && role_id.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(NewAssignment {
|
||||
user_id,
|
||||
user_group_id,
|
||||
role_id,
|
||||
})
|
||||
Some(NewAssignment { user_id, role_id })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,15 +43,14 @@ impl NewAssignment {
|
||||
let assignment = sqlx::query_as!(
|
||||
Assignment,
|
||||
r#"
|
||||
INSERT INTO posthog_errortrackingissueassignment (id, issue_id, user_id, user_group_id, role_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
INSERT INTO posthog_errortrackingissueassignment (id, issue_id, user_id, role_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (issue_id) DO UPDATE SET issue_id = $2 -- no-op to get a returned row
|
||||
RETURNING id, issue_id, user_id, user_group_id, role_id, created_at
|
||||
RETURNING id, issue_id, user_id, role_id, created_at
|
||||
"#,
|
||||
Uuid::now_v7(),
|
||||
issue_id,
|
||||
self.user_id,
|
||||
self.user_group_id,
|
||||
self.role_id,
|
||||
).fetch_one(conn).await?;
|
||||
|
||||
@@ -73,7 +63,6 @@ pub struct Assignment {
|
||||
pub id: Uuid,
|
||||
pub issue_id: Uuid,
|
||||
pub user_id: Option<i32>,
|
||||
pub user_group_id: Option<Uuid>,
|
||||
pub role_id: Option<Uuid>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -109,7 +98,6 @@ pub struct AssignmentRule {
|
||||
pub id: Uuid,
|
||||
pub team_id: TeamId,
|
||||
pub user_id: Option<i32>,
|
||||
pub user_group_id: Option<Uuid>,
|
||||
pub role_id: Option<Uuid>,
|
||||
pub order_key: i32,
|
||||
pub bytecode: Value,
|
||||
@@ -127,7 +115,7 @@ impl AssignmentRule {
|
||||
sqlx::query_as!(
|
||||
AssignmentRule,
|
||||
r#"
|
||||
SELECT id, team_id, user_id, user_group_id, role_id, order_key, bytecode, created_at, updated_at
|
||||
SELECT id, team_id, user_id, role_id, order_key, bytecode, created_at, updated_at
|
||||
FROM posthog_errortrackingassignmentrule
|
||||
WHERE team_id = $1 AND disabled_data IS NULL
|
||||
"#,
|
||||
@@ -207,11 +195,7 @@ impl AssignmentRule {
|
||||
let step_result = vm.step()?;
|
||||
match step_result {
|
||||
StepOutcome::Finished(Value::Bool(true)) => {
|
||||
return Ok(NewAssignment::try_new(
|
||||
self.user_id,
|
||||
self.user_group_id,
|
||||
self.role_id,
|
||||
));
|
||||
return Ok(NewAssignment::try_new(self.user_id, self.role_id));
|
||||
}
|
||||
StepOutcome::Finished(Value::Bool(false)) => {
|
||||
return Ok(None);
|
||||
|
||||
@@ -26,7 +26,6 @@ pub struct GroupingRule {
|
||||
|
||||
// If a rule has been custom grouped, it might also be auto-assigned
|
||||
pub user_id: Option<i32>,
|
||||
pub user_group_id: Option<Uuid>,
|
||||
pub role_id: Option<Uuid>,
|
||||
pub order_key: i32,
|
||||
pub bytecode: Value,
|
||||
@@ -44,7 +43,7 @@ impl GroupingRule {
|
||||
sqlx::query_as!(
|
||||
GroupingRule,
|
||||
r#"
|
||||
SELECT id, team_id, user_id, user_group_id, role_id, order_key, bytecode, created_at, updated_at
|
||||
SELECT id, team_id, user_id, role_id, order_key, bytecode, created_at, updated_at
|
||||
FROM posthog_errortrackinggroupingrule
|
||||
WHERE team_id = $1 AND disabled_data IS NULL
|
||||
"#,
|
||||
@@ -131,7 +130,7 @@ impl GroupingRule {
|
||||
}
|
||||
|
||||
pub fn assignment(&self) -> Option<NewAssignment> {
|
||||
NewAssignment::try_new(self.user_id, self.user_group_id, self.role_id)
|
||||
NewAssignment::try_new(self.user_id, self.role_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +210,6 @@ mod test {
|
||||
id: Uuid::new_v4(),
|
||||
team_id: 1,
|
||||
user_id: None,
|
||||
user_group_id: None,
|
||||
role_id: None,
|
||||
order_key: 1,
|
||||
bytecode: rule_bytecode(),
|
||||
|
||||
@@ -182,7 +182,7 @@ impl Issue {
|
||||
let assignments = sqlx::query_as!(
|
||||
Assignment,
|
||||
r#"
|
||||
SELECT id, issue_id, user_id, user_group_id, role_id, created_at FROM posthog_errortrackingissueassignment
|
||||
SELECT id, issue_id, user_id, role_id, created_at FROM posthog_errortrackingissueassignment
|
||||
WHERE issue_id = $1
|
||||
"#,
|
||||
self.id
|
||||
|
||||
@@ -36,7 +36,6 @@ fn get_test_rule() -> AssignmentRule {
|
||||
id: Uuid::new_v4(),
|
||||
team_id: 1,
|
||||
user_id: Some(1), // This rule assigns the issue to user with ID 1
|
||||
user_group_id: None,
|
||||
role_id: None,
|
||||
order_key: 1,
|
||||
bytecode: rule_bytecode(),
|
||||
@@ -125,14 +124,13 @@ async fn test_assignment_processing(db: PgPool) {
|
||||
assert!(res.is_some());
|
||||
let res = res.unwrap();
|
||||
assert_eq!(res.user_id, existing.user_id);
|
||||
assert_eq!(res.user_group_id, existing.user_group_id);
|
||||
assert_eq!(res.role_id, existing.role_id);
|
||||
|
||||
// Next, change the issue, and put an assignment on the fingerprint. The returned assignment should be the one from
|
||||
// the fingerprint, rather than the rule, because fingerprint assignments take priority over assignment rules
|
||||
|
||||
let mut props_with_fingerprint_assignment = test_props.clone();
|
||||
let fingerprint_assignment = NewAssignment::try_new(Some(3), None, None).unwrap();
|
||||
let fingerprint_assignment = NewAssignment::try_new(Some(3), None).unwrap();
|
||||
props_with_fingerprint_assignment.fingerprint.assignment = Some(fingerprint_assignment);
|
||||
|
||||
let mut new_issue = issue.clone();
|
||||
@@ -150,6 +148,5 @@ async fn test_assignment_processing(db: PgPool) {
|
||||
assert!(res.is_some());
|
||||
let res = res.unwrap();
|
||||
assert_eq!(res.user_id, Some(3));
|
||||
assert_eq!(res.user_group_id, None);
|
||||
assert_eq!(res.role_id, None);
|
||||
}
|
||||
|
||||