From dcd81825e4bdce31383c844caa91c96e13542868 Mon Sep 17 00:00:00 2001 From: Yusuke Nemoto Date: Fri, 3 Jan 2025 15:00:10 +0900 Subject: [PATCH] Update webui with GitHub link and chart page (#6) * Update webui with GitHub link and chart page Add GitHub link icon and new chart page to web UI. * **Add GitHub link icon** - Add a GitHub link icon to the top-right corner of all pages in `webapp/index.html`. - Link the icon to the GitHub repository URL. * **Create ChartPage component** - Add a new file `webapp/src/components/ChartPage.tsx` to create a React component for displaying the chart. - Use Chart.js to render the chart. - Load data from `assets/pr_counts_by_date.csv` and display it as a line chart. * **Update main.tsx for routing** - Modify `webapp/src/main.tsx` to include routing for the new chart page. - Import and use the `ChartPage` component. * **Update README.md** - Add instructions for accessing the new chart page. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/kaakaa/ai-agent-statistics?shareId=XXXX-XXXX-XXXX-XXXX). * Add chart data fetching and routing for ChartPage component * **ChartPage.tsx** - Replace d3-fetch with fetch API to load CSV data - Parse CSV data and update chartData state * **main.tsx** - Replace `Switch` with `Routes` for routing - Update `Route` components to use `element` prop instead of `component` prop * feat: support subpath settings * feat: add new chart page * Update webapp/src/components/statistics/PullRequestsCount.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update webapp/src/components/statistics/PullRequestsCount.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 6 ++ webapp/index.html | 10 +++ webapp/package-lock.json | 76 ++++++++++++++++++- webapp/package.json | 5 +- webapp/src/components/Statistics.tsx | 55 ++++++++++++++ .../statistics/PullRequestsCount.tsx | 34 +++++++++ .../src/components/statistics/ReposCount.tsx | 34 +++++++++ webapp/src/main.tsx | 11 ++- webapp/src/types.ts | 6 ++ 9 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 webapp/src/components/Statistics.tsx create mode 100644 webapp/src/components/statistics/PullRequestsCount.tsx create mode 100644 webapp/src/components/statistics/ReposCount.tsx create mode 100644 webapp/src/types.ts diff --git a/README.md b/README.md index 121b747..b912bb6 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,10 @@ $ npm run preview => access to http://localhost:4173/ai-agent-statistics ``` +## Accessing the Chart Page +To view the chart of PR counts by date, follow these steps: + +1. Open your web browser. +2. Navigate to the following URL: `http://localhost:4173/ai-agent-statistics/chart` +3. You will see a line chart displaying the PR counts by date. diff --git a/webapp/index.html b/webapp/index.html index 4cc08a8..fdd06d0 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -4,8 +4,18 @@ AI-Agent Statistics + + + GitHub +
diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 3ddda03..813978d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -15,8 +15,11 @@ "@mui/material": "^6.3.0", "@mui/x-data-grid": "^7.23.5", "@tanstack/react-table": "^8.20.6", + "chart.js": "^4.4.7", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-chartjs-2": "^5.3.0", + "react-dom": "^18.3.1", + "react-router": "^7.1.1" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1089,6 +1092,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.0.tgz", @@ -1747,6 +1755,11 @@ "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==" }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2245,6 +2258,17 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2325,6 +2349,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3391,6 +3423,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -3417,6 +3458,29 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3557,6 +3621,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3683,6 +3752,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index 83cbc66..29c4c45 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,8 +18,11 @@ "@mui/material": "^6.3.0", "@mui/x-data-grid": "^7.23.5", "@tanstack/react-table": "^8.20.6", + "chart.js": "^4.4.7", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-chartjs-2": "^5.3.0", + "react-dom": "^18.3.1", + "react-router": "^7.1.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/webapp/src/components/Statistics.tsx b/webapp/src/components/Statistics.tsx new file mode 100644 index 0000000..6b144e6 --- /dev/null +++ b/webapp/src/components/Statistics.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js'; + +import { PRCount } from '../types'; +import PullRequestsCountChart from './statistics/PullRequestsCount'; +import ReposCountChart from './statistics/ReposCount'; + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); + +export type ChartDataType = { + labels: string[]; + datasets: { + label: string; + data: number[]; + borderColor: string; + backgroundColor: string + }[]; +} + +const StatisticsPage = () => { + const [prCounts, setPrCounts] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const basepath = import.meta.env.BASE_URL + const baseUrl = `${window.location.protocol}//${window.location.host}${basepath}`.replace(/\/$/, ''); + console.log(`fetch pr_counts_by_date.csv from baseUrl: ${baseUrl}`); + const resp = await fetch(`${baseUrl}/assets/pr_counts_by_date.csv`); + + const csv = await resp.text(); + const prCounts: PRCount[]= csv.split('\n').slice(1).map(row => { + const values = row.split(','); + return { + date: values[0], + author: values[1], + count: parseInt(values[2], 10), + repos: parseInt(values[3], 10), + } + }); + setPrCounts(prCounts); + }; + fetchData(); + }, []); + + return ( +
+

PR Count by Date

+ +

Repos Count by Date

+ +
+ ); +}; + +export default StatisticsPage; diff --git a/webapp/src/components/statistics/PullRequestsCount.tsx b/webapp/src/components/statistics/PullRequestsCount.tsx new file mode 100644 index 0000000..9613238 --- /dev/null +++ b/webapp/src/components/statistics/PullRequestsCount.tsx @@ -0,0 +1,34 @@ +import { Line } from 'react-chartjs-2'; + +import { PRCount } from '../../types'; + +type ChartProps = { + prCounts: PRCount[]; +} + +const PullRequestsCountChart = ({prCounts}: ChartProps) => { + const authors = [ + {name: "devin-ai-integration", color: 'rgba(0, 180, 170, 1)'}, + {name: "devloai", color: 'rgba(0, 122, 255, 1)'}, + {name: "openhands-agent", color: 'rgba(255, 204, 0, 1)'}, + ]; + const labels = Array.from(new Set(prCounts.map((prCount: PRCount) => prCount.date))); + + const datasets = authors.map(author => { + const countsByAuthor = prCounts.filter((prCount: PRCount) => prCount.author === author.name) + const counts = labels.map(date => countsByAuthor.find((c: PRCount) => c.date === date)?.count || 0); + return { + label: author.name, + data: counts, + borderColor: author.color, + backgroundColor: 'rgba(75, 192, 192, 0.2)', + } + }); + return ( + <> + + + ) +} + +export default PullRequestsCountChart; \ No newline at end of file diff --git a/webapp/src/components/statistics/ReposCount.tsx b/webapp/src/components/statistics/ReposCount.tsx new file mode 100644 index 0000000..b56b313 --- /dev/null +++ b/webapp/src/components/statistics/ReposCount.tsx @@ -0,0 +1,34 @@ +import { Line } from 'react-chartjs-2'; + +import { PRCount } from '../../types'; + +type ChartProps = { + prCounts: PRCount[]; +} + +const ReposCountChart = ({prCounts}: ChartProps) => { + const authors = [ + {name: "devin-ai-integration", color: 'rgba(0, 180, 170, 1)'}, + {name: "devloai", color: 'rgba(0, 122, 255, 1)'}, + {name: "openhands-agent", color: 'rgba(255, 204, 0, 1)'}, + ]; + const labels = Array.from(new Set(prCounts.map((prCount: any) => prCount.date))); + + const datasets = authors.map(author => { + const countsByAuthor = prCounts.filter((prCount: any) => prCount.author === author.name) + const counts = labels.map(date => countsByAuthor.find((c: PRCount) => c.date === date)?.repos || 0); + return { + label: author.name, + data: counts, + borderColor: author.color, + backgroundColor: 'rgba(75, 192, 192, 0.2)', + } + }); + return ( + <> + + + ) +} + +export default ReposCountChart; \ No newline at end of file diff --git a/webapp/src/main.tsx b/webapp/src/main.tsx index dfc35d8..ecb68a8 100644 --- a/webapp/src/main.tsx +++ b/webapp/src/main.tsx @@ -1,10 +1,19 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter as Router, Route, Routes } from 'react-router'; import './index.css' import PullRequestsTable from './components/PullRequests.tsx' +import StatisticsPage from './components/Statistics.tsx'; + +const base = import.meta.env.BASE_URL createRoot(document.getElementById('root')!).render( - + + + } /> + } /> + + , ) diff --git a/webapp/src/types.ts b/webapp/src/types.ts new file mode 100644 index 0000000..afe5ae6 --- /dev/null +++ b/webapp/src/types.ts @@ -0,0 +1,6 @@ +export type PRCount = { + date: string; + author: string; + count: number; + repos: number; +}