From e21aade6145bcd87131b57f34f120fcdfc39f3e2 Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Tue, 13 Aug 2024 03:51:55 +0530 Subject: [PATCH] feat/ activity table and details --- .../activity/AllActivityDetails.tsx | 119 ++++++++++-------- .../activity/AllActivityTable.tsx | 64 +++++----- .../activity/page.tsx | 22 ++-- .../projects/page.tsx | 4 +- .../(specific-project-pages)/AllRunsTable.tsx | 2 +- .../[projectSlug]/runs/[runId]/page.tsx | 22 +++- src/data/user/projects.tsx | 64 ++++++++++ src/data/user/runs.ts | 45 +++++++ 8 files changed, 243 insertions(+), 99 deletions(-) diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityDetails.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityDetails.tsx index 4e6e79b9..0bb8e04b 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityDetails.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityDetails.tsx @@ -1,23 +1,25 @@ -'use client'; +'use client' -import { getRunsByProjectId } from "@/data/user/runs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { getAllRunsByOrganizationId } from "@/data/user/runs"; import { supabaseUserClientComponentClient } from "@/supabase-clients/user/supabaseUserClientComponentClient"; import { useQuery } from "@tanstack/react-query"; +import { motion } from "framer-motion"; import { useEffect } from "react"; +import { AllActivityTable } from "./AllActivityTable"; export default function AllActivityDetails({ - projectId, - projectSlug + organizationId, + allowedProjectIdsForUser }: { - projectId: string; - projectSlug: string; + organizationId: string; + allowedProjectIdsForUser: string[]; }) { - const { data: runs, refetch, isLoading } = useQuery( - ['runs', projectId], + ['runs', organizationId], async () => { - console.log('runs fetching'); - return getRunsByProjectId(projectId); + return getAllRunsByOrganizationId(organizationId); }, { refetchOnWindowFocus: false, @@ -25,40 +27,49 @@ export default function AllActivityDetails({ ); useEffect(() => { - const channel = supabaseUserClientComponentClient - .channel('digger_runs_realtime') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'digger_runs', - filter: `project_id=eq.${projectId}` - }, - (payload) => { - refetch(); - } - ) - .on( - 'postgres_changes', - { - event: 'UPDATE', - schema: 'public', - table: 'digger_runs', - filter: `project_id=eq.${projectId}` - }, - (payload) => { - refetch(); - }, - ) - .subscribe(); + const channels: ReturnType[] = []; + + if (runs) { + const projectIds = Array.from(new Set(runs.map(run => run.project_id))); + + projectIds.forEach(projectId => { + const channel = supabaseUserClientComponentClient + .channel(`digger_runs_realtime_${projectId}`) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'digger_runs', + filter: `project_id=eq.${projectId}` + }, + (payload) => { + refetch(); + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'digger_runs', + filter: `project_id=eq.${projectId}` + }, + (payload) => { + refetch(); + }, + ) + .subscribe(); + + channels.push(channel); + }); + } return () => { - channel.unsubscribe(); + channels.forEach(channel => channel.unsubscribe()); }; - }, [projectId, refetch]); + }, [runs, refetch]); - /* if (isLoading) { return (
@@ -76,18 +87,20 @@ export default function AllActivityDetails({ ); } - if (!runs) { + if (!runs || runs.length === 0) { return - Project Runs - View all runs for this project + Activity + View all activity in this organization -

No runs found

+

No activity found

} + const filteredRuns = runs.filter(run => allowedProjectIdsForUser.includes(run.project_id)); + return ( - - Project Runs - View all runs for this project - + Organization Activity + View all activity for this organization @@ -114,14 +125,16 @@ export default function AllActivityDetails({ animate={{ opacity: 1 }} transition={{ duration: 0.15, delay: 0.2 }} > - + ({ + ...run, + project_name: run.project_name || run.projects?.name || 'Unknown', + project_slug: run.projects?.slug || 'Unknown', + }))} + allowedRunsForUser={filteredRuns.map(run => run.id)} + /> ); - */ - return ( -
TEST
- ) } \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityTable.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityTable.tsx index 73e5f01e..94b632ee 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityTable.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/AllActivityTable.tsx @@ -1,33 +1,27 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Tables } from "@/lib/database.types"; import { ToSnakeCase } from "@/lib/utils"; import { AnimatePresence, motion } from "framer-motion"; import { Activity } from "lucide-react"; import moment from "moment"; import Link from "next/link"; +import { statusColors } from "../../../../project/[projectSlug]/(specific-project-pages)/AllRunsTable"; -export type StatusColor = { - [key: string]: string; -}; - -export const statusColors: StatusColor = { - queued: 'bg-yellow-200/50 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200', - pending_plan: 'bg-amber-200/50 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200', - running_plan: 'bg-purple-200/50 text-purple-800 dark:bg-purple-900/50 dark:text-purple-200', - pending_approval: 'bg-blue-200/50 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200', - approved: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', - pending_apply: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', - running_apply: 'bg-indigo-200/50 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200', - succeeded: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', - failed: 'bg-red-200/50 text-red-800 dark:bg-red-900/50 dark:text-red-200', - discarded: 'bg-neutral-200 text-neutral-800 dark:bg-neutral-950 dark:text-neutral-200', -}; - - -export const AllActivityTable = ({ runs, projectSlug }: { runs: Tables<'digger_runs'>[], projectSlug: string }) => { +export const AllActivityTable = ({ runs, allowedRunsForUser }: { + runs: { + id: string; + commit_id: string; + status: string; + updated_at: string; + project_id: string; + project_slug: string; + project_name: string; + repo_id: number; + approver_user_name: string | null; + }[] + allowedRunsForUser: string[] +}) => { const sortedRuns = [...runs].sort((a, b) => { - // Sort primarily by created_at in descending order (most recent first) - return moment(b.created_at).valueOf() - moment(a.created_at).valueOf(); + return moment(b.updated_at).valueOf() - moment(a.updated_at).valueOf(); }); return ( @@ -38,6 +32,7 @@ export const AllActivityTable = ({ runs, projectSlug }: { runs: Tables<'digger_r Commit ID Status Last updated + Project ID User @@ -53,20 +48,29 @@ export const AllActivityTable = ({ runs, projectSlug }: { runs: Tables<'digger_r transition={{ duration: 0.3 }} > - - + {allowedRunsForUser.includes(run.id) ? ( + + + {run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id} + + + ) : ( + {run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id} - + )} {run.commit_id} - - {run.status.toUpperCase()} - + + + {run.status.toUpperCase()} + + {moment(run.updated_at).fromNow()} - {run.approval_author} + {run.project_name} + {run.approver_user_name} )) ) : ( @@ -86,7 +90,7 @@ export const AllActivityTable = ({ runs, projectSlug }: { runs: Tables<'digger_r

- Runs will appear here once they are initiated. Note you need to setup your repo with digger_workflow.yml to be able to trigger runs, for more information refer to the which includes example workflow file Docs quickstart + No activity found for this organization. Runs will appear here once they are initiated.

diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/page.tsx index f7a5ebc5..a2fe7fec 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/activity/page.tsx @@ -1,8 +1,10 @@ import { PageHeading } from "@/components/PageHeading"; import { T } from "@/components/ui/Typography"; +import { getLoggedInUserOrganizationRole } from "@/data/user/organizations"; +import { getProjectsIdsListForUser } from "@/data/user/projects"; +import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser"; import { - organizationParamSchema, - projectsfilterSchema + organizationParamSchema } from "@/utils/zod-schemas/params"; import type { Metadata } from "next"; import { Suspense } from "react"; @@ -10,8 +12,8 @@ import type { DashboardProps } from "../page"; import AllActivityDetails from "./AllActivityDetails"; export const metadata: Metadata = { - title: "Teams", - description: "You can create teams within your organization.", + title: "Activity", + description: "Activity in all projects of this organization", }; export default async function Page({ @@ -19,23 +21,27 @@ export default async function Page({ searchParams, }: DashboardProps) { const { organizationId } = organizationParamSchema.parse(params); - const filters = projectsfilterSchema.parse(searchParams); + const [{ id }, userRole, allowedProjectIdsForUser] = await Promise.all([ + serverGetLoggedInUser(), + getLoggedInUserOrganizationRole(organizationId), + getProjectsIdsListForUser({ userId: (await serverGetLoggedInUser()).id, userRole: await getLoggedInUserOrganizationRole(organizationId), organizationId }) + ]); return (
- Loading teams... + Loading activity... } > - +
); diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx index ca61329c..619ceb8b 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx @@ -11,7 +11,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; import type { DashboardProps } from "../page"; -import { AllProjectsTableWithPagination } from "./ProjectsWithPagination"; +import { UserProjectsWithPagination } from "./ProjectsWithPagination"; export const metadata: Metadata = { title: "Projects", @@ -57,7 +57,7 @@ export default async function Page({ } > - diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx index 0fd26aff..ff9601ef 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/AllRunsTable.tsx @@ -20,7 +20,7 @@ export const statusColors: StatusColor = { running_apply: 'bg-indigo-200/50 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200', succeeded: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', failed: 'bg-red-200/50 text-red-800 dark:bg-red-900/50 dark:text-red-200', - discarded: 'bg-neutral-200 text-neutral-800 dark:bg-neutral-950 dark:text-neutral-200', + discarded: 'bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200', }; diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx index 050ed0cc..6d908287 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/runs/[runId]/page.tsx @@ -2,7 +2,7 @@ import { PageHeading } from "@/components/PageHeading"; import { T } from "@/components/ui/Typography"; import { getLoggedInUserOrganizationRole } from "@/data/user/organizations"; -import { getSlimProjectById } from "@/data/user/projects"; +import { getProjectsIdsListForUser, getSlimProjectById } from "@/data/user/projects"; import { getRepoDetails } from "@/data/user/repos"; import { getBatchIdFromApplyStageId, getBatchIdFromPlanStageId, getOutputLogsAndWorkflowURLFromBatchId, getRunById, getTFOutputAndWorkflowURLFromBatchId } from "@/data/user/runs"; import { getUserProfile } from "@/data/user/user"; @@ -13,6 +13,7 @@ import { } from "@/utils/zod-schemas/params"; import type { Metadata } from "next"; import dynamic from 'next/dynamic'; +import { redirect } from "next/navigation"; import { ComponentType, Suspense } from "react"; export const metadata: Metadata = { @@ -54,10 +55,12 @@ export default async function RunDetailPage({ }: RunDetailPageProps) { const { runId } = runIdParamSchema.parse(params); + + // Fetch run and user data in parallel const [run, user] = await Promise.all([ getRunById(runId), - serverGetLoggedInUser() + serverGetLoggedInUser(), ]); const project_id = run.project_id; @@ -78,17 +81,26 @@ export default async function RunDetailPage({ const [organizationRole, planBatchId, applyBatchId] = await Promise.all([ getLoggedInUserOrganizationRole(project.organization_id), getBatchIdFromPlanStageId(run.plan_stage_id), - getBatchIdFromApplyStageId(run.apply_stage_id) + getBatchIdFromApplyStageId(run.apply_stage_id), + ]); // Fetch terraform outputs and workflow URLs in parallel - const [planData, applyData] = await Promise.all([ + const [planData, applyData, projectsIdsForUser] = await Promise.all([ getTFOutputAndWorkflowURLFromBatchId(planBatchId), - getOutputLogsAndWorkflowURLFromBatchId(applyBatchId) + getOutputLogsAndWorkflowURLFromBatchId(applyBatchId), + getProjectsIdsListForUser({ userId: user.id, userRole: organizationRole, organizationId: project.organization_id }) ]); const isOrganizationAdmin = organizationRole === "admin" || organizationRole === "owner"; + + if (!projectsIdsForUser.includes(project_id)) { + return ( + redirect(`/org/${project.organization_id}`) + ) + } + return (
; + organizationId: string; + query?: string; + teamIds?: number[]; +}): Promise { + const supabase = createSupabaseUserServerComponentClient(); + + let supabaseQuery = supabase + .from('projects') + .select('id,name, slug, latest_action_on, created_at, repo_id') + .eq('organization_id', organizationId) + .ilike('name', `%${query}%`); + + if (userRole !== 'admin' || userId !== 'owner') { + // For non-admin users, get their team memberships + const { data: userTeams } = await supabase + .from('team_members') + .select('team_id') + .eq('user_id', userId); + + const userTeamIds = userTeams?.map(team => team.team_id) || []; + + // Filter by user's teams and organization-level projects + supabaseQuery = supabaseQuery.or(`team_id.is.null,team_id.in.(${userTeamIds.join(',')})`); + } + + // Apply team filter if provided + if (teamIds.length > 0) { + supabaseQuery = supabaseQuery.in('team_id', teamIds); + } + + const { data, error } = await supabaseQuery.order('latest_action_on', { + ascending: false, + }); + + if (error) { + console.error("Error fetching projects:", error); + return []; + } + + if (!data) return []; + + // Fetch repo details for each project + const projectsWithRepoDetails = await Promise.all( + data.map(async (project) => { + const repoDetails = await getRepoDetails(project.repo_id); + const { repo_id, ...projectWithoutRepoId } = project; + return { + ...projectWithoutRepoId, + repo_full_name: repoDetails?.repo_full_name || null, + }; + }) + ); + + return projectsWithRepoDetails.map(project => project.id); +} export async function getProjectsCountForUser({ userId, diff --git a/src/data/user/runs.ts b/src/data/user/runs.ts index 7f774773..4f1b3ed9 100644 --- a/src/data/user/runs.ts +++ b/src/data/user/runs.ts @@ -124,6 +124,51 @@ export async function getRunsByProjectId(projectId: string) { return data; } +export async function getAllRunsByOrganizationId(organizationId: string) { + const supabase = createSupabaseUserServerComponentClient(); + + const { data: runs, error } = await supabase + .from('digger_runs') + .select( + ` + id, + commit_id, + status, + updated_at, + project_id, + repo_id, + approver_user_id, + projects(name, slug) + `, + ) + .eq('projects.organization_id', organizationId) + .order('updated_at', { ascending: false }); + + if (error) throw error; + + // Fetch user profiles for approvers + const approverIds = runs.map((run) => run.approver_user_id).filter(Boolean); + const { data: approvers, error: approversError } = await supabase + .from('user_profiles') + .select('id, user_name') + .in('id', approverIds); + + if (approversError) throw approversError; + + // Create a map of approver ids to user names + const approverMap = new Map( + approvers.map((approver) => [approver.id, approver.user_name]), + ); + + return runs.map((run) => ({ + ...run, + project_name: run.projects?.name ?? null, + approver_user_name: run.approver_user_id + ? (approverMap.get(run.approver_user_id) ?? null) + : null, + })); +} + export async function requestRunApproval( runId: string, ): Promise> {