diff --git a/app/core/components/GitHub/GitHubActivity/index.tsx b/app/core/components/GitHub/GitHubActivity/index.tsx new file mode 100644 index 00000000..1a9efcd1 --- /dev/null +++ b/app/core/components/GitHub/GitHubActivity/index.tsx @@ -0,0 +1,51 @@ +import { Card, CardContent, TableContainer, Table, TableHead, TableRow, TableCell, TableBody } from "@mui/material"; + +interface gitHubActivitySchema { + id: string, + typeEvent: string, + created_at: Date, + author: string, + avatar_url:string, + projectId:string | null, +} + +export default function GitHubActivity({ repoName, projectId, activityData }: { repoName: string, projectId: string, activityData: gitHubActivitySchema[] }) { + return ( + + + + + + + Event Type + Author + Created At + + + + + { + activityData && activityData.map(event => ( + + + {event.typeEvent} + + {event.author} + { new Intl.DateTimeFormat([], { + year: "numeric", + month: "long", + day: "2-digit", + }).format(new Date(event.created_at))} + + )) + } + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/githubUpdates.server.ts b/app/githubUpdates.server.ts new file mode 100644 index 00000000..002f87c2 --- /dev/null +++ b/app/githubUpdates.server.ts @@ -0,0 +1,31 @@ +import { getActivity } from "./routes/api/github/get-proyectActivity"; +import { PrismaClient } from "@prisma/client"; + +const db = new PrismaClient(); + + +// export async function searchLastUpdateProjects() { + +// const lastUpdate = await prisma.$queryRaw`SELECT DISTINCT p."id", p."name", p."projectBoard", MAX(ga.created_at) from "Projects" p +// RIGHT JOIN "GitHubActivity" ga on p."id" = ga."projectId" +// WHERE p."isArchived" = FALSE +// GROUP BY p."id", p."name", p."projectBoard" +// `; + + +// } I will use it later jeje + +export async function getGitHubActivity() { + const projectsBoards = await db.$queryRaw` + SELECT p."id", p."name", r.url from "Projects" p + RIGHT JOIN "Repos" r on p."id" = r."projectId" + WHERE p."isArchived" = FALSE and r."url" is not null + `; + + try{ + projectsBoards.map(async board => await getActivity(board.url, board.id).catch((e) => {throw(e)}) ) + }catch(e){ + throw (e); + } + +} diff --git a/app/models/githubactivity.server.ts b/app/models/githubactivity.server.ts new file mode 100644 index 00000000..7254c692 --- /dev/null +++ b/app/models/githubactivity.server.ts @@ -0,0 +1,47 @@ +import { prisma } from "../db.server"; +import type { PrismaClient } from "@prisma/client"; + +interface gitHubActivityChartType { + count: number, + typeEvent: string, +} + +export async function saveActivity( + id: string, + typeEvent: string, + created_at: string, + author: string, + avatar_url: string, + projectId: string, + db?: PrismaClient + ){ + + const dbConnection = db ? db : prisma; + + const activityRegister = await dbConnection.gitHubActivity.findFirst({ where: { id } }); + + if(!activityRegister) { + + return await dbConnection.gitHubActivity.create({ + data: { + id, + typeEvent, + created_at: new Date(created_at), + author, + avatar_url, + projectId + } + }) + } + } + + +export async function getGitActivityData(projectId: string) { + return await prisma.gitHubActivity.findMany({ where: { projectId }, orderBy: { id: "desc" }}); +} + +export const getActivityStadistic = async (week: number) => { + return await prisma.$queryRaw`SELECT Count(*)::int, "typeEvent" FROM "GitHubActivity" where date_part('week', "created_at")=${week} GROUP BY "typeEvent"`; + +} + diff --git a/app/routes/api/github/get-proyectActivity.tsx b/app/routes/api/github/get-proyectActivity.tsx new file mode 100644 index 00000000..b3f3efc2 --- /dev/null +++ b/app/routes/api/github/get-proyectActivity.tsx @@ -0,0 +1,48 @@ +import { Octokit } from "@octokit/core"; +import { env } from "process"; +import { saveActivity } from "../../../models/githubactivity.server"; +import { PrismaClient } from "@prisma/client"; +const octokit = new Octokit({ auth: env.GITHUB_KEY }); + +const db = new PrismaClient(); + +function cleanUrlRepo(repoInfo: string) { + if (repoInfo) { + return repoInfo.substring(repoInfo.lastIndexOf("/") + 1); + } else { + return ""; + } +} + +export const getActivity = async (repo: string, projectId: string) => { + + const owner = "wizeline"; + const repoUrlClean = cleanUrlRepo(repo); + if(repo != ''){ + try{ + const repoActivity = await octokit.request(`GET /repos/${owner}/${repoUrlClean}/events`, { + owner, + repo, + }).catch((e) => { throw(e)}); + + + if(repoActivity.data.length){ + repoActivity.data?.forEach( (activity: { id: string; type: string; created_at: string; actor: { display_login: string; avatar_url: string; }; }) => { + saveActivity(activity.id , + activity.type?.replace(/([a-z0-9])([A-Z])/g, '$1 $2') as string, //this is for separe the string with camel case into pieces + activity.created_at as string, activity.actor.display_login as string, + activity.actor.avatar_url as string, projectId, db ); + return; + }); + } + + + }catch(e){ + console.error(e); + } + + }else{ + return; + } +}; + diff --git a/app/routes/projects/$projectId/github-info/index.tsx b/app/routes/projects/$projectId/github-info/index.tsx index 84f4f3eb..d97ce796 100644 --- a/app/routes/projects/$projectId/github-info/index.tsx +++ b/app/routes/projects/$projectId/github-info/index.tsx @@ -1,66 +1,258 @@ -import { CircularProgress, Container, Grid, Paper, Stack, Typography } from "@mui/material"; -import type { LoaderArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import invariant from "tiny-invariant"; -import Header from "~/core/layouts/Header"; -import { getProject } from "~/models/project.server"; -import GitHub from '@mui/icons-material/GitHub'; -import GoBack from "~/core/components/GoBack"; - - - -export const loader = async ({ request, params }: LoaderArgs) => { - invariant(params.projectId, "projectId not found"); - const projectId = params.projectId; - - const project = await getProject({ id: params.projectId }); - if (!project) { - throw new Response("Not Found", { status: 404 }); + import { Alert, Button, Container, Grid, InputLabel, MenuItem, Paper, Select, Stack, Typography } from "@mui/material"; + import type { LoaderFunction } from "@remix-run/server-runtime"; + import { json } from "@remix-run/node"; + import invariant from "tiny-invariant"; + import Header from "~/core/layouts/Header"; + import { getProject } from "~/models/project.server"; + import GoBack from "~/core/components/GoBack"; + import { GitHub, Refresh } from "@mui/icons-material"; + import type { Repos } from "@prisma/client"; + import GitHubActivity from "~/core/components/GitHub/GitHubActivity"; + import { getActivityStadistic, getGitActivityData } from "~/models/githubactivity.server"; + import { Bar } from "react-chartjs-2"; + import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + } from 'chart.js'; +import { week, currentdate, numberOfDays } from "~/utils"; +import { ValidatedForm } from "remix-validated-form"; +import { withZod } from "@remix-validated-form/with-zod"; +import { zfd } from "zod-form-data"; +import { z } from "zod"; +import { useLoaderData, useSubmit } from "@remix-run/react"; + ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend + ); + + + type LoaderData = { + project: Awaited>, + projectId: string, + activityData: Awaited>, + activityChartData: Awaited>, + weekParams: number + }; + + interface gitHubActivityChartType { + count: number, + typeEvent: string, + } + + export const validator = withZod( + zfd.formData({ + body: z.string().min(1), + parentId: z.string().optional().nullable(), + id: z.string().optional(), + }) + ); + + export const loader: LoaderFunction = async ({ request, params }) => { + invariant(params.projectId, "projectId not found"); + const url = new URL(request.url); + let weekParams = 0; + weekParams = parseInt(url.searchParams.get("week") as string); + const projectId = params.projectId; + const project = await getProject({ id: params.projectId }); + if (!project) { + throw new Response("Not Found", { status: 404 }); + } + const activityData = await getGitActivityData(projectId); + const activityChartData:gitHubActivityChartType[] = await getActivityStadistic(weekParams ? weekParams : week); + + return json({ + project, + projectId, + activityData, + activityChartData, + weekParams + }); + }; + + + const cleanURL = (repoInfo: Repos[]):string => { + + if (repoInfo[0] && repoInfo[0].url !== '') { + return repoInfo[0].url.substring(repoInfo[0].url.lastIndexOf("/") + 1); + } else { + return ""; + } } - - return typedjson({ - project, - projectId - }); - }; - - -export default function GitHubInfo() { - - const { - project,projectId - } = useTypedLoaderData(); - - return <> -
- - - - - -

{project.name}

- - Last commit: - + + export default function GitHubInfo() { + const submit = useSubmit(); + + const itemsSelect = []; + + const { project, projectId, activityData, activityChartData, weekParams } = useLoaderData(); + + let week = Math.ceil(( currentdate.getDay() + 1 + numberOfDays) / 7); + let selectedWeek = weekParams ? weekParams : week; + + for (let index = 1; index <= week; index++) { + itemsSelect.push({index}) + } + + const dataChart = { + labels: activityChartData.map( (activity: { typeEvent: any; }) => activity.typeEvent), + datasets: [ { + data: activityChartData.map( (activity: { count: any; }) => Number(activity.count)), + backgroundColor: "#3B72A4", + borderColor: "#3B72A4", + }], + + }; + + const options = { + responsive: true, + color: "#A7C7DC", + plugins: { + title: { + display: true, + text: `Events per Week`, + color: "#A7C7DC" + }, + legend: { + display: false, + + }, + }, + scales: { + x: { + title: { + display: false + }, + grid: { + display:false + }, + ticks: { + color: "#A7C7DC" + } + }, + y: { + title: { + display: false + }, + ticks: { + color: "#4BA4E1", + }, + grid: { + color: '#6C7176' + } + } + } + + }; + + const handleSubmit = async (event: any) => { + + const body = { + week: event.target.value, + }; + + submit(body); + + } + + + + + return <> +
+ + + + + +

{project.name}

+ + Last commit: + +
+ +
+
+
+ + + + + + + Project Participation + +