[WEB-2358] chore: optimised the recent collaborators endpoint (#5470)

* chore: optimised the recent collaborators endpoint

* chore: recent collabators code refactor

* chore: sorted the user's based on active issues

* chore: recent collaborators sorting

* chore: code refactor

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Bavisetti Narayan 2024-09-05 15:38:10 +05:30 committed by GitHub
parent bf49ebb519
commit 3d7098855f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 49 additions and 320 deletions

View file

@ -574,105 +574,42 @@ def dashboard_recent_projects(self, request, slug):
def dashboard_recent_collaborators(self, request, slug):
# Subquery to count activities for each project member
activity_count_subquery = (
IssueActivity.objects.filter(
workspace__slug=slug,
actor=OuterRef("member"),
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.values("actor")
.annotate(num_activities=Count("pk"))
.values("num_activities")
)
# Get all project members and annotate them with activity counts
project_members_with_activities = (
ProjectMember.objects.filter(
WorkspaceMember.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
is_active=True,
)
.annotate(
num_activities=Coalesce(
Subquery(activity_count_subquery),
Value(0),
output_field=IntegerField(),
),
is_current_user=Case(
When(member=request.user, then=Value(0)),
default=Value(1),
output_field=IntegerField(),
active_issue_count=Count(
Case(
When(
member__issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
member__issue_assignee__issue__workspace__slug=slug,
member__issue_assignee__issue__project__project_projectmember__member=request.user,
member__issue_assignee__issue__project__project_projectmember__is_active=True,
then=F("member__issue_assignee__issue__id"),
),
distinct=True,
output_field=IntegerField(),
),
distinct=True,
),
user_id=F("member_id"),
)
.values_list("member", flat=True)
.order_by("is_current_user", "-num_activities")
.values("user_id", "active_issue_count")
.order_by("-active_issue_count")
.distinct()
)
search = request.query_params.get("search", None)
if search:
project_members_with_activities = (
project_members_with_activities.filter(
Q(member__display_name__icontains=search)
| Q(member__first_name__icontains=search)
| Q(member__last_name__icontains=search)
)
)
return self.paginate(
request=request,
queryset=project_members_with_activities,
controller=lambda qs: self.get_results_controller(qs, slug),
return Response(
(project_members_with_activities),
status=status.HTTP_200_OK,
)
class DashboardEndpoint(BaseAPIView):
def get_results_controller(self, project_members_with_activities, slug):
user_active_issue_counts = (
User.objects.filter(
id__in=project_members_with_activities,
)
.annotate(
active_issue_count=Count(
Case(
When(
issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
issue_assignee__issue__workspace__slug=slug,
issue_assignee__issue__project__project_projectmember__is_active=True,
then=F("issue_assignee__issue__id"),
),
output_field=IntegerField(),
),
distinct=True,
)
)
.values("active_issue_count", user_id=F("id"))
)
# Create a dictionary to store the active issue counts by user ID
active_issue_counts_dict = {
user["user_id"]: user["active_issue_count"]
for user in user_active_issue_counts
}
# Preserve the sequence of project members with activities
paginated_results = [
{
"user_id": member_id,
"active_issue_count": active_issue_counts_dict.get(
member_id, 0
),
}
for member_id in project_members_with_activities
]
return paginated_results
def create(self, request, slug):
serializer = DashboardSerializer(data=request.data)
if serializer.is_valid():

View file

@ -145,17 +145,8 @@ export type TRecentActivityWidgetResponse = IIssueActivity;
export type TRecentProjectsWidgetResponse = string[];
export type TRecentCollaboratorsWidgetResponse = {
count: number;
extra_stats: Object | null;
next_cursor: string;
next_page_results: boolean;
prev_cursor: string;
prev_page_results: boolean;
results: {
active_issue_count: number;
user_id: string;
}[];
total_pages: number;
active_issue_count: number;
user_id: string;
};
export type TWidgetStatsResponse =
@ -166,7 +157,7 @@ export type TWidgetStatsResponse =
| TCreatedIssuesWidgetResponse
| TRecentActivityWidgetResponse[]
| TRecentProjectsWidgetResponse
| TRecentCollaboratorsWidgetResponse;
| TRecentCollaboratorsWidgetResponse[];
// dashboard
export type TDashboard = {

View file

@ -1,6 +1,6 @@
"use client";
import { useEffect } from "react";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
@ -52,64 +52,50 @@ const CollaboratorListItem: React.FC<CollaboratorListItemProps> = observer((prop
});
type CollaboratorsListProps = {
cursor: string;
dashboardId: string;
perPage: number;
searchQuery?: string;
updateIsLoading?: (isLoading: boolean) => void;
updateResultsCount: (count: number) => void;
updateTotalPages: (count: number) => void;
workspaceSlug: string;
};
const WIDGET_KEY = "recent_collaborators";
export const CollaboratorsList: React.FC<CollaboratorsListProps> = (props) => {
const {
cursor,
dashboardId,
perPage,
searchQuery = "",
updateIsLoading,
updateResultsCount,
updateTotalPages,
workspaceSlug,
} = props;
const { dashboardId, searchQuery = "", workspaceSlug } = props;
// store hooks
const { fetchWidgetStats } = useDashboard();
const { getUserDetails } = useMember();
const { data: currentUser } = useUser();
const { data: widgetStats } = useSWR(
workspaceSlug && dashboardId && cursor
? `WIDGET_STATS_${workspaceSlug}_${dashboardId}_${cursor}_${searchQuery}`
: null,
workspaceSlug && dashboardId && cursor
workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null,
workspaceSlug && dashboardId
? () =>
fetchWidgetStats(workspaceSlug, dashboardId, {
cursor,
per_page: perPage,
search: searchQuery,
widget_key: WIDGET_KEY,
})
: null
) as {
data: TRecentCollaboratorsWidgetResponse | undefined;
data: TRecentCollaboratorsWidgetResponse[] | undefined;
};
useEffect(() => {
updateIsLoading?.(true);
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
if (!widgetStats) return;
const sortedStats = sortBy(widgetStats, [(user) => user.user_id !== currentUser?.id]);
updateIsLoading?.(false);
updateTotalPages(widgetStats.total_pages);
updateResultsCount(widgetStats.results?.length);
}, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]);
const filteredStats = sortedStats.filter((user) => {
const { display_name, first_name, last_name } = getUserDetails(user.user_id) || {};
if (!widgetStats || !widgetStats?.results) return <WidgetLoader widgetKey={WIDGET_KEY} />;
const searchLower = searchQuery.toLowerCase();
return (
display_name?.toLowerCase().includes(searchLower) ||
first_name?.toLowerCase().includes(searchLower) ||
last_name?.toLowerCase().includes(searchLower)
);
});
return (
<>
{widgetStats?.results?.map((user) => (
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
{filteredStats?.map((user) => (
<CollaboratorListItem
key={user.user_id}
issueCount={user.active_issue_count}
@ -117,6 +103,6 @@ export const CollaboratorsList: React.FC<CollaboratorsListProps> = (props) => {
workspaceSlug={workspaceSlug}
/>
))}
</>
</div>
);
};

View file

@ -1,76 +0,0 @@
"use client";
import { useState } from "react";
// components
import { Button } from "@plane/ui";
import { CollaboratorsList } from "./collaborators-list";
// ui
type Props = {
dashboardId: string;
perPage: number;
workspaceSlug: string;
};
export const DefaultCollaboratorsList: React.FC<Props> = (props) => {
const { dashboardId, perPage, workspaceSlug } = props;
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
const handleLoadMore = () => setPageCount((prev) => prev + 1);
const updateTotalPages = (count: number) => setTotalPages(count);
const updateResultsCount = (count: number) => setResultsCount(count);
const collaboratorsPages: JSX.Element[] = [];
for (let i = 0; i < pageCount; i++)
collaboratorsPages.push(
<CollaboratorsList
key={i}
dashboardId={dashboardId}
cursor={`${perPage}:${i}:0`}
perPage={perPage}
updateResultsCount={updateResultsCount}
updateTotalPages={updateTotalPages}
workspaceSlug={workspaceSlug}
/>
);
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
const showViewLessButton = pageCount > 1;
return (
<>
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
{collaboratorsPages}
</div>
{(showViewLessButton || showViewMoreButton) && (
<div className="flex items-center justify-center text-xs w-full">
{showViewLessButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={() => setPageCount(1)}
>
View less
</Button>
)}
{showViewMoreButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={handleLoadMore}
>
View more
</Button>
)}
</div>
)}
</>
);
};

View file

@ -4,10 +4,7 @@ import { Search } from "lucide-react";
import { Card } from "@plane/ui";
import { WidgetProps } from "@/components/dashboard/widgets";
// components
import { DefaultCollaboratorsList } from "./default-list";
import { SearchedCollaboratorsList } from "./search-list";
const PER_PAGE = 8;
import { CollaboratorsList } from "./collaborators-list";
export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
const { dashboardId, workspaceSlug } = props;
@ -33,16 +30,7 @@ export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
/>
</div>
</div>
{searchQuery.trim() !== "" ? (
<SearchedCollaboratorsList
dashboardId={dashboardId}
perPage={PER_PAGE}
searchQuery={searchQuery}
workspaceSlug={workspaceSlug}
/>
) : (
<DefaultCollaboratorsList dashboardId={dashboardId} perPage={PER_PAGE} workspaceSlug={workspaceSlug} />
)}
<CollaboratorsList dashboardId={dashboardId} searchQuery={searchQuery} workspaceSlug={workspaceSlug} />
</Card>
);
};

View file

@ -1,97 +0,0 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// components
// ui
import { Button } from "@plane/ui";
// assets
import DarkImage from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg";
import LightImage from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg";
import { CollaboratorsList } from "./collaborators-list";
type Props = {
dashboardId: string;
perPage: number;
searchQuery: string;
workspaceSlug: string;
};
export const SearchedCollaboratorsList: React.FC<Props> = (props) => {
const { dashboardId, perPage, searchQuery, workspaceSlug } = props;
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
// next-themes
const { resolvedTheme } = useTheme();
const handleLoadMore = () => setPageCount((prev) => prev + 1);
const updateTotalPages = (count: number) => setTotalPages(count);
const updateResultsCount = (count: number) => setResultsCount(count);
const collaboratorsPages: JSX.Element[] = [];
for (let i = 0; i < pageCount; i++)
collaboratorsPages.push(
<CollaboratorsList
key={i}
dashboardId={dashboardId}
cursor={`${perPage}:${i}:0`}
perPage={perPage}
searchQuery={searchQuery}
updateIsLoading={setIsLoading}
updateResultsCount={updateResultsCount}
updateTotalPages={updateTotalPages}
workspaceSlug={workspaceSlug}
/>
);
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
const showViewLessButton = pageCount > 1;
const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage;
return (
<>
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
{collaboratorsPages}
</div>
{!isLoading && totalPages === 0 && (
<div className="flex flex-col items-center gap-6 mb-8">
<div className="h-24 w-24 flex-shrink-0">
<Image src={emptyStateImage} className="w-full h-full" alt="Recent collaborators" />
</div>
<p className="font-medium text-sm">No matching member</p>
</div>
)}
{(showViewLessButton || showViewMoreButton) && (
<div className="flex items-center justify-center text-xs w-full">
{showViewLessButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={() => setPageCount(1)}
>
View less
</Button>
)}
{showViewMoreButton && (
<Button
variant="link-primary"
size="sm"
className="my-3 hover:bg-custom-primary-100/20"
onClick={handleLoadMore}
>
View more
</Button>
)}
</div>
)}
</>
);
};