Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions label_studio/data_manager/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ class TaskListAPI(generics.ListCreateAPIView):
)
pagination_class = TaskPagination

def get_task_serializer_context(self, request, project, queryset):
@staticmethod
def get_task_serializer_context(request, project):
all_fields = request.GET.get('fields', None) == 'all' # false by default

return {
Expand Down Expand Up @@ -327,6 +328,7 @@ def get(self, request):
# get prepare params (from view or from payload directly)
prepare_params = get_prepare_params(request, project)
queryset = self.get_task_queryset(request, prepare_params)
context = self.get_task_serializer_context(self.request, project)

# paginated tasks
page = self.paginate_queryset(queryset)
Expand All @@ -341,15 +343,16 @@ def get(self, request):
all_fields = None
if page is not None:
ids = [task.id for task in page] # page is a list already
tasks = self.prefetch(
Task.prepared.annotate_queryset(
Task.objects.filter(id__in=ids),
fields_for_evaluation=fields_for_evaluation,
all_fields=all_fields,
request=request,
tasks = list(
self.prefetch(
Task.prepared.annotate_queryset(
Task.objects.filter(id__in=ids),
fields_for_evaluation=fields_for_evaluation,
all_fields=all_fields,
request=request,
)
)
)

tasks_by_ids = {task.id: task for task in tasks}
# keep ids ordering
page = [tasks_by_ids[_id] for _id in ids]
Expand All @@ -364,7 +367,6 @@ def get(self, request):
evaluate_predictions(tasks_for_predictions)
[tasks_by_ids[_id].refresh_from_db() for _id in ids]

context = self.get_task_serializer_context(self.request, project, tasks)
serializer = self.task_serializer_class(page, many=True, context=context)
return self.get_paginated_response(serializer.data)
# all tasks
Expand All @@ -373,7 +375,6 @@ def get(self, request):
queryset = Task.prepared.annotate_queryset(
queryset, fields_for_evaluation=fields_for_evaluation, all_fields=all_fields, request=request
)
context = self.get_task_serializer_context(self.request, project, queryset)
serializer = self.task_serializer_class(queryset, many=True, context=context)
return Response(serializer.data)

Expand Down
3 changes: 2 additions & 1 deletion label_studio/tasks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
self.task = self.get_object()

def prefetch(self, queryset):
@staticmethod
def prefetch(queryset):
return queryset.prefetch_related(
'annotations',
'predictions',
Expand Down
2 changes: 1 addition & 1 deletion label_studio/tests/sdk/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_export_tasks(django_live_url, business_client):
single_task = ls.tasks.get(id=task_id)
assert single_task.data['my_text'] == 'Test task 7'
assert single_task.total_annotations == 1
assert single_task.updated_by[0]['user_id'] == business_client.user.id
assert single_task.updated_by == [{'user_id': business_client.user.id}]

exported_tasks = [task for task in ls.tasks.list(project=p.id, fields='all') if task.annotations]
assert len(exported_tasks) == 1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { inject } from "mobx-react";
import clsx from "clsx";
import { useMemo } from "react";
import { useSDK } from "../../../providers/SDKProvider";
import { cn } from "../../../utils/bem";
import { isDefined } from "../../../utils/utils";
Expand All @@ -18,33 +17,16 @@ const isFilterMembers = isActive(FF_DM_FILTER_MEMBERS);
export const Annotators = (cell) => {
const { value, column, original: task } = cell;
const sdk = useSDK();
const maxUsersToDisplay = window.APP_SETTINGS.data_manager.max_users_to_display;
const userList = Array.from(value).slice(0, maxUsersToDisplay);
const userList = Array.from(value);
const renderable = userList.slice(0, 10);
const extra = userList.length - renderable.length;
const userPickBadge = cn("userpic-badge");
const annotatorsCN = cn("annotators");
const isEnterprise = window.APP_SETTINGS.billing?.enterprise;

// Memoize the count field calculation
const extraCount = useMemo(() => {
const getCountField = () => {
switch (column.alias) {
case "annotators":
return task?.annotators_count || 0;
case "reviewers":
return task?.reviewers_count || 0;
case "comment_authors":
return task?.comment_authors_count || 0;
default:
return 0;
}
};

return getCountField() - maxUsersToDisplay;
}, [column.alias, task?.annotators_count, task?.reviewers_count, task?.comment_authors_count]);

return (
<div className={annotatorsCN.toString()}>
{userList.map((item, index) => {
{renderable.map((item, index) => {
const user = item.user ?? item;
const { annotated, reviewed, review } = item;

Expand Down Expand Up @@ -78,7 +60,7 @@ export const Annotators = (cell) => {
</div>
);
})}
{extraCount > 0 && (
{extra > 0 && (
<div
className={annotatorsCN.elem("item").toString()}
onClick={(e) => {
Expand All @@ -87,7 +69,7 @@ export const Annotators = (cell) => {
sdk.invoke("userCellCounterClick", e, column.alias, task, userList);
}}
>
<Userpic addCount={`+${extraCount}`} />
<Userpic addCount={`+${extra}`} />
</div>
)}
</div>
Expand Down Expand Up @@ -120,11 +102,6 @@ Annotators.FilterItem = UsersInjector(({ item }) => {

Annotators.searchFilter = (option, queryString) => {
const user = DM.usersMap.get(option?.value);
if (!user) {
// Fallback to searching by ID if user not found
return option?.value?.toString().toLowerCase().includes(queryString.toLowerCase());
}

return (
user.id?.toString().toLowerCase().includes(queryString.toLowerCase()) ||
user.email.toLowerCase().includes(queryString.toLowerCase()) ||
Expand Down
15 changes: 2 additions & 13 deletions web/libs/datamanager/src/stores/AppStore.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { destroy, flow, types } from "mobx-state-tree";
import { Modal } from "../components/Common/Modal/Modal";
import {
FF_DEV_2887,
FF_DISABLE_GLOBAL_USER_FETCHING,
FF_LOPS_E_3,
FF_REGION_VISIBILITY_FROM_URL,
isFF,
} from "../utils/feature-flags";
import { FF_DEV_2887, FF_LOPS_E_3, FF_REGION_VISIBILITY_FROM_URL, isFF } from "../utils/feature-flags";
import { History } from "../utils/history";
import { isDefined } from "../utils/utils";
import { Action } from "./Action";
Expand Down Expand Up @@ -575,12 +569,7 @@ export const AppStore = types

self.viewsStore.fetchColumns();

const requests = [self.fetchProject()];

// Only fetch all users if not disabled globally
if (!isFF(FF_DISABLE_GLOBAL_USER_FETCHING)) {
requests.push(self.fetchUsers());
}
const requests = [self.fetchProject(), self.fetchUsers()];

if (!isLabelStream || (self.project?.show_annotation_history && task)) {
if (self.SDK.type === "dm") {
Expand Down
32 changes: 5 additions & 27 deletions web/libs/datamanager/src/stores/Assignee.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
import { types } from "mobx-state-tree";
import { User } from "./Users";
import { StringOrNumberID } from "./types";
import { FF_DISABLE_GLOBAL_USER_FETCHING, isFF } from "../utils/feature-flags";

// Create a union type that can handle both user references and direct user objects
const UserOrReference = types.union({
dispatcher: (snapshot) => {
// If it's a full user object (has firstName, email, etc.), use User model
if (snapshot && typeof snapshot === "object" && (snapshot.firstName || snapshot.email || snapshot.username)) {
return User;
}
// Otherwise, it's a reference to a user ID
return types.reference(User);
},
cases: {
[User.name]: User,
reference: types.reference(User),
},
});

export const Assignee = types
.model("Assignee", {
id: StringOrNumberID,
user: types.late(() => UserOrReference),
user: types.late(() => types.reference(User)),
review: types.maybeNull(types.enumeration(["accepted", "rejected", "fixed"])),
reviewed: types.maybeNull(types.boolean),
annotated: types.maybeNull(types.boolean),
Expand Down Expand Up @@ -65,17 +48,12 @@ export const Assignee = types
reviewed: false,
};
} else {
const { user_id, annotated, review, reviewed, ...user } = sn;
const { user_id, user, ...rest } = sn;

// When global user fetching is disabled, always create user objects, otherwise use references via user id
// If we only have user_id and no other user properties, just use the user_id as reference
const hasUserProperties = Object.keys(user).length > 0;
result = {
id: user_id,
user: isFF(FF_DISABLE_GLOBAL_USER_FETCHING) && hasUserProperties ? { id: user_id, ...user } : user_id, // Use user_id as reference
annotated,
review,
reviewed,
...rest,
id: user_id ?? user,
user: user_id ?? user,
};
}

Expand Down
9 changes: 1 addition & 8 deletions web/libs/datamanager/src/stores/DataStores/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isDefined } from "../../utils/utils";
import { Assignee } from "../Assignee";
import { DynamicModel, registerModel } from "../DynamicModel";
import { CustomJSON } from "../types";
import { FF_DEV_2536, FF_DISABLE_GLOBAL_USER_FETCHING, FF_LOPS_E_3, isFF } from "../../utils/feature-flags";
import { FF_DEV_2536, FF_LOPS_E_3, isFF } from "../../utils/feature-flags";

const SIMILARITY_UPPER_LIMIT_PRECISION = 1000;
const fileAttributes = types.model({
Expand Down Expand Up @@ -36,13 +36,6 @@ export const create = (columns) => {
allow_postpone: types.maybeNull(types.boolean),
unique_lock_id: types.maybeNull(types.string),
updated_by: types.optional(types.array(Assignee), []),
...(isFF(FF_DISABLE_GLOBAL_USER_FETCHING)
? {
annotators_count: types.optional(types.number, 0),
reviewers_count: types.optional(types.number, 0),
comment_authors_count: types.optional(types.number, 0),
}
: {}),
...(isFF(FF_LOPS_E_3)
? {
_additional: types.optional(fileAttributes, {}),
Expand Down
7 changes: 0 additions & 7 deletions web/libs/datamanager/src/utils/feature-flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,6 @@ export const FF_AVERAGE_AGREEMENT_SCORE_POPOVER = "fflag_feat_all_leap_2042_aver
*/
export const FF_ANNOTATION_RESULTS_FILTERING = "fflag_root_13_annotation_results_filtering";

/**
* Disable global user fetching for large-scale deployments
* @link https://app.launchdarkly.com/projects/default/flags/fflag_all_feat_utc_204_users_performance_improvements_in_dm_for_large_orgs
*/
export const FF_DISABLE_GLOBAL_USER_FETCHING =
"fflag_all_feat_utc_204_users_performance_improvements_in_dm_for_large_orgs";

// Customize flags
const flags = {};

Expand Down
28 changes: 6 additions & 22 deletions web/libs/editor/src/stores/Annotation/Annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,26 +94,6 @@ const TrackedState = types.model("TrackedState", {
relationStore: types.optional(RelationStore, {}),
});

// Create a union type that can handle both user references and frozen user objects
const UserOrReference = types.union({
dispatcher: (snapshot) => {
// If it's a number, it's a reference to a user ID
if (typeof snapshot === "number") {
return types.safeReference(UserExtended);
}
// If it's a full user object, store it as frozen to avoid duplicate instances
if (snapshot && typeof snapshot === "object" && (snapshot.firstName || snapshot.email || snapshot.username)) {
return types.frozen();
}
// Default to reference for any other case
return types.safeReference(UserExtended);
},
cases: {
frozen: types.frozen(),
reference: types.safeReference(UserExtended),
},
});

const _Annotation = types
.model("AnnotationBase", {
id: types.identifier,
Expand All @@ -129,7 +109,7 @@ const _Annotation = types
createdDate: types.optional(types.string, Utils.UDate.currentISODate()),
createdAgo: types.maybeNull(types.string),
createdBy: types.optional(types.string, "Admin"),
user: types.optional(types.maybeNull(UserOrReference), null),
user: types.optional(types.maybeNull(types.safeReference(UserExtended)), null),
score: types.maybeNull(types.number),

parent_prediction: types.maybeNull(types.integer),
Expand Down Expand Up @@ -189,7 +169,7 @@ const _Annotation = types
}))
.preProcessSnapshot((sn) => {
// sn.draft = Boolean(sn.draft);
const user = sn.user ?? sn.completed_by ?? undefined;
let user = sn.user ?? sn.completed_by ?? undefined;
let root;

const updateIds = (item) => {
Expand All @@ -210,6 +190,10 @@ const _Annotation = types
root = updateIds(sn.root.toJSON());
}

if (user && typeof user !== "number") {
user = user.id;
}

const getCreatedBy = (snapshot) => {
if (snapshot.type === "prediction") {
const modelVersion = snapshot.model_version?.trim() ?? "";
Expand Down
4 changes: 1 addition & 3 deletions web/libs/ui/src/lib/Userpic/Userpic.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@

.username {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
line-height: 1;
font-weight: bold;
text-align: center;
Expand Down Expand Up @@ -72,4 +70,4 @@
&.faded .username {
opacity: 0.2;
}
}
}
Loading