Skip to content

Commit

Permalink
Merge pull request #22 from diggerhq/feat/activity
Browse files Browse the repository at this point in the history
Duplicate runs views
  • Loading branch information
ZIJ authored Aug 13, 2024
2 parents 66bc7c5 + d3227bd commit 3c63604
Show file tree
Hide file tree
Showing 13 changed files with 615 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { FreeTrialComponent } from '@/components/SubscriptionCards';
import { Skeleton } from '@/components/ui/skeleton';
import { getIsStripeTestMode } from '@/utils/server/stripe-utils';
import { differenceInDays } from 'date-fns';
import { FileText, Home, Layers, MessageCircle, Users } from 'lucide-react';
import { Activity, FileText, Home, Layers, MessageCircle, Users } from 'lucide-react';


const isStripeTestMode = getIsStripeTestMode()
Expand Down Expand Up @@ -91,6 +91,11 @@ async function OrganizationSidebarInternal({
href={`/org/${organizationId}/teams`}
icon={<Users className="size-4 text-foreground" />}
/>
<SidebarLink
label="Activity"
href={`/org/${organizationId}/activity`}
icon={<Activity className="size-4 text-foreground" />}
/>
<SidebarLink
label="Docs"
href={`https://docs.digger.dev/team/getting-started/gha-aws`}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use client'

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({
organizationId,
allowedProjectIdsForUser
}: {
organizationId: string;
allowedProjectIdsForUser: string[];
}) {
const { data: runs, refetch, isLoading } = useQuery(
['runs', organizationId],
async () => {
return getAllRunsByOrganizationId(organizationId);
},
{
refetchOnWindowFocus: false,
}
);

useEffect(() => {
const channels: ReturnType<typeof supabaseUserClientComponentClient.channel>[] = [];

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 () => {
channels.forEach(channel => channel.unsubscribe());
};
}, [runs, refetch]);

if (isLoading) {
return (
<div className="w-full">
<div className="space-y-4">
{[...Array(3)].map((_, index) => (
<div key={index} className="flex space-x-4">
<Skeleton className="h-12 w-1/4" />
<Skeleton className="h-12 w-1/4" />
<Skeleton className="h-12 w-1/4" />
<Skeleton className="h-12 w-1/4" />
</div>
))}
</div>
</div>
);
}

if (!runs || runs.length === 0) {
return <Card>
<CardHeader>
<CardTitle>Activity</CardTitle>
<CardDescription>View all activity in this organization</CardDescription>
</CardHeader>
<CardContent>
<p>No activity found</p>
</CardContent>
</Card>
}

const filteredRuns = runs.filter(run => allowedProjectIdsForUser.includes(run.project_id));

return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.1 }}
>
<Card className="w-full">
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15, delay: 0.1 }}
>
<CardHeader>
<CardTitle>Organization Activity</CardTitle>
<CardDescription>View all activity for this organization</CardDescription>
</CardHeader>
</motion.div>
<CardContent>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15, delay: 0.2 }}
>
<AllActivityTable runs={runs.map(run => ({
...run,
repo_full_name: run.repos?.repo_full_name || 'Unknown repository',
project_name: run.project_name || run.projects?.name || 'Unknown',
project_slug: run.projects?.slug || 'Unknown',
}))}
allowedRunsForUser={filteredRuns.map(run => run.id)}
/>
</motion.div>
</CardContent>
</Card>
</motion.div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
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 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;
repo_full_name: string;
approver_user_name: string | null;
}[]
allowedRunsForUser: string[]
}) => {
const sortedRuns = [...runs].sort((a, b) => {
return moment(b.updated_at).valueOf() - moment(a.updated_at).valueOf();
});

return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-left">Run ID</TableHead>
<TableHead className="text-left">Commit ID</TableHead>
<TableHead className="text-left">Status</TableHead>
<TableHead className="text-left">Last updated</TableHead>
<TableHead className="text-left">Project name</TableHead>
<TableHead className="text-left">Approver</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<AnimatePresence>
{sortedRuns.length > 0 ? (
sortedRuns.map((run) => (
<motion.tr
key={run.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
>
<TableCell>
{allowedRunsForUser.includes(run.id) ? (
<Link href={`/project/${run.project_slug}/runs/${run.id}`} className="hover:underline cursor-pointer">
<span>
{run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id}
</span>
</Link>
) : (
<span>
{run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id}
</span>
)}
</TableCell>
{allowedRunsForUser.includes(run.id) ?
<TableCell>
<Link href={`https://github.com/${run.repo_full_name}/commit/${run.commit_id}`}
className="hover:underline cursor-pointer">
{run.commit_id.substring(0, 8)}
</Link>
</TableCell>
:
<TableCell>{run.commit_id.substring(0, 8)}</TableCell>
}
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[ToSnakeCase(run.status)] || ''}`}>
{run.status.toUpperCase()}
</span>
</TableCell>
<TableCell>{moment(run.updated_at).fromNow()}</TableCell>
<TableCell>{run.project_name}</TableCell>
<TableCell>{run.approver_user_name || "Unknown"}</TableCell>
</motion.tr>
))
) : (
<TableRow>
<TableCell colSpan={5} className="w-full justify-center">
<motion.div
className="flex flex-col items-center justify-center mx-auto max-w-96 h-64 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
className="rounded-full bg-gray-100 p-4 dark:bg-gray-800"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Activity className="h-8 w-8 text-gray-400" />
</motion.div>
<p className="mt-2 text-sm text-foreground">
No activity found for this organization. Runs will appear here once they are initiated.
</p>
</motion.div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
</TableBody>
</Table>
);
};
Loading

0 comments on commit 3c63604

Please sign in to comment.