From 601e38859da2bf3c74a002cdb66f8eb1568b8f78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:40:09 +0000 Subject: [PATCH 01/78] Bump @babel/traverse from 7.22.5 to 7.23.2 in /who-metrics-ui Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.5 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- who-metrics-ui/package-lock.json | 167 +++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/who-metrics-ui/package-lock.json b/who-metrics-ui/package-lock.json index 95cfad5..b1aef0d 100644 --- a/who-metrics-ui/package-lock.json +++ b/who-metrics-ui/package-lock.json @@ -73,16 +73,81 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", @@ -144,11 +209,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -212,20 +277,20 @@ "peer": true }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -293,9 +358,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -312,9 +377,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -343,12 +408,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -420,9 +485,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -456,31 +521,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -497,12 +562,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { From fc72b08aa32c98256557fef27dcfed270b930def Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 9 Nov 2023 18:47:17 +0000 Subject: [PATCH 02/78] Update development instructions Since the frontend will soon require a `data.json` file to be present for the build step to succeed, this updates the instructions for developing the UI. We added a `make dev` command which builds the backend, fetches and generates a `data.json` file, and then runs the Next development server. Co-authored-by: Andrew Henry --- Makefile | 6 +++++- README.md | 15 +++++++++------ dev.vscode.env.example | 3 ++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 3090af0..29ed32f 100644 --- a/Makefile +++ b/Makefile @@ -40,4 +40,8 @@ metrics: test-go: @echo "==> running Go tests <==" - CGO_ENABLED=1 go test -p 64 -race ./backend/... + CGO_ENABLED=1 cd backend && go test -p 64 -race ./... + +dev: + @echo "==> Generating data" + cd backend && go build -o ./bin/metrics ./cmd && cd .. && ./backend/bin/metrics && cd who-metrics-ui && npm i && npm run dev diff --git a/README.md b/README.md index ae09a59..c1611d8 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,6 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss ## Development in Codespaces -### UI - -1. Run `cd who-metrics-ui && npm i` -2. Run `npm run dev` - ### Backend @@ -84,4 +79,12 @@ make build ./backend/bin/metrics ``` -This will generate a new `data.json` file in the UI directory which can be imported directly as part of the static build. \ No newline at end of file +This will generate a new `data.json` file in the UI directory which can be imported directly as part of the static build. + +### UI + +Run `make dev` to develop in the UI. This will: + +1. Build the backend project using the steps above +1. Generate a new `data.json` file with the required data inside the frontend directory +1. Run the Next development server diff --git a/dev.vscode.env.example b/dev.vscode.env.example index 708db11..6a8feda 100644 --- a/dev.vscode.env.example +++ b/dev.vscode.env.example @@ -1 +1,2 @@ -GITHUB_GRAPHQL_TOKEN="TOKEN" +GRAPHQL_TOKEN="" +ORGANIZATION_NAME="your_organization" From 69df0a9e931a5eae36f807a0b607083f7882b02e Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 9 Nov 2023 20:52:40 +0000 Subject: [PATCH 03/78] Render basic repositories table This starts to actually incorporate the data we generate into the frontend by creating a basic repositories table to replace the placeholder "PerformanceHistoryTable" that was already there. Right away, this lets you: - View all of the public repositories - Filter by license type - Filter by repository name We also update the `DashboardExample` to read the name of the Organization from the data file to make the Organization Name a bit more dynamic. We'll need to figure out how to do something for the logo, but going to hold off on that for now. --- who-metrics-ui/.eslintrc.js | 1 + .../src/components/DashboardExample.tsx | 14 +- .../components/PerformanceHistoryTable.tsx | 170 ------------------ .../src/components/RepositoriesTable.tsx | 125 +++++++++++++ who-metrics-ui/src/data/types.d.ts | 23 --- 5 files changed, 135 insertions(+), 198 deletions(-) delete mode 100644 who-metrics-ui/src/components/PerformanceHistoryTable.tsx create mode 100644 who-metrics-ui/src/components/RepositoriesTable.tsx delete mode 100644 who-metrics-ui/src/data/types.d.ts diff --git a/who-metrics-ui/.eslintrc.js b/who-metrics-ui/.eslintrc.js index 7a79ae9..14d7402 100644 --- a/who-metrics-ui/.eslintrc.js +++ b/who-metrics-ui/.eslintrc.js @@ -225,6 +225,7 @@ module.exports = { rules: { "i18n-text/no-en": "off", "filenames/match-regex": [2, "^[A-Z][a-zA-Z]+(.[a-z0-9-]+)?$"], + "import/extensions": "off", }, }, { diff --git a/who-metrics-ui/src/components/DashboardExample.tsx b/who-metrics-ui/src/components/DashboardExample.tsx index 7ed732c..4fe87bb 100644 --- a/who-metrics-ui/src/components/DashboardExample.tsx +++ b/who-metrics-ui/src/components/DashboardExample.tsx @@ -19,7 +19,8 @@ import logo from "@/images/who-logo-wide.svg"; import { ChartView } from "./"; import KpiCard from "./KpiCard"; -import { PerformanceHistoryTable } from "./PerformanceHistoryTable"; +import RepositoriesTable from "./RepositoriesTable"; +import Data from "../data/data.json"; type Kpi = { title: string; @@ -103,11 +104,14 @@ export const DashboardExample = () => { src={logo} height={50} width={150} - alt="who logo" + alt="World Health Organization logo" /> - Dashboard + {Data.orgInfo.name} Open Source Dashboard - Lorem ipsum dolor sit amet, consetetur sadipscing elitr. + + This project includes metrics about the Open Source repositories for the + {Data.orgInfo.name}. + Overview @@ -135,7 +139,7 @@ export const DashboardExample = () => { - + diff --git a/who-metrics-ui/src/components/PerformanceHistoryTable.tsx b/who-metrics-ui/src/components/PerformanceHistoryTable.tsx deleted file mode 100644 index 11c4fc1..0000000 --- a/who-metrics-ui/src/components/PerformanceHistoryTable.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { InfoIcon } from "@primer/octicons-react"; -import { Tooltip } from "@primer/react"; -import { - BadgeDelta, - Card, - DeltaType, - Select, - Flex, - MultiSelect, - MultiSelectItem, - SelectItem, - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, - Title, -} from "@tremor/react"; -import { useState } from "react"; - -const deltaTypes: { [key: string]: DeltaType } = { - average: "unchanged", - overperforming: "moderateIncrease", - underperforming: "moderateDecrease", -}; - -type SalesPerson = { - name: string; - leads: number; - sales: string; - quota: string; - variance: string; - region: string; - status: string; -}; - -const salesPeople: SalesPerson[] = [ - { - name: "Peter Doe", - leads: 45, - sales: "1,000,000", - quota: "1,200,000", - variance: "low", - region: "Region A", - status: "overperforming", - }, - { - name: "Lena Whitehouse", - leads: 35, - sales: "900,000", - quota: "1,000,000", - variance: "low", - region: "Region B", - status: "average", - }, - { - name: "Phil Less", - leads: 52, - sales: "930,000", - quota: "1,000,000", - variance: "medium", - region: "Region C", - status: "underperforming", - }, - { - name: "John Camper", - leads: 22, - sales: "390,000", - quota: "250,000", - variance: "low", - region: "Region A", - status: "overperforming", - }, - { - name: "Max Balmoore", - leads: 49, - sales: "860,000", - quota: "750,000", - variance: "low", - region: "Region B", - status: "overperforming", - }, -]; -export const PerformanceHistoryTable = () => { - const [selectedStatus, setSelectedStatus] = useState("all"); - const [selectedNames, setSelectedNames] = useState([]); - - const isSalesPersonSelected = (salesPerson: SalesPerson) => - (salesPerson.status === selectedStatus || selectedStatus === "all") && - (selectedNames.includes(salesPerson.name) || selectedNames.length === 0); - return ( - - <> -
- - Performance History - - - - -
-
- - {salesPeople.map((item) => ( - - {item.name} - - ))} - - -
- - - - Name - Leads - - Sales ($) - - - Quota ($) - - Variance - Region - Status - - - - - {salesPeople - .filter((item) => isSalesPersonSelected(item)) - .map((item) => ( - - {item.name} - {item.leads} - {item.sales} - {item.quota} - {item.variance} - {item.region} - - - {item.status} - - - - ))} - -
- -
- ); -}; diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx new file mode 100644 index 0000000..c61b02e --- /dev/null +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -0,0 +1,125 @@ +import { InfoIcon } from "@primer/octicons-react"; +import { Tooltip } from "@primer/react"; +import { + Card, + Select, + Flex, + MultiSelect, + MultiSelectItem, + SelectItem, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Title, +} from "@tremor/react"; +import { useState } from "react"; +import Data from "../data/data.json"; + +const repos = Object.values(Data["repositories"]); +const licenses = repos + .map((repo) => repo.licenseName) + .filter((licenseName, index, array) => array.indexOf(licenseName) === index); +type Repo = (typeof repos)[0]; + +const Labels: Record = { + Name: "repositoryName", + Collaborators: "collaboratorsCount", + License: "licenseName", + Watchers: "watchersCount", + "Open Issues": "openIssuesCount", +} as const; + +const headers = Object.keys(Labels); + +const RepositoriesTable = () => { + const [selectedLicense, setSelectedStatus] = useState("all"); + const [selectedNames, setSelectedNames] = useState([]); + const isRepoSelected = (repo: Repo) => + (repo.licenseName === selectedLicense || selectedLicense === "all") && + (selectedNames.includes(repo.repositoryName) || selectedNames.length === 0); + return ( + + <> +
+ + Repositories + + + + +
+
+ + {repos.map((item) => ( + + {item.repositoryName} + + ))} + + +
+ + + + {headers.map((label, index) => ( + + {label} + + ))} + + + + + {repos + .filter((repo) => isRepoSelected(repo)) + .map((repo) => ( + + {headers.map((header, index) => { + const property = Labels[header]; + const value = repo[property]; + return ( + + {value} + + ); + })} + + ))} + +
+ +
+ ); +}; + +export default RepositoriesTable; diff --git a/who-metrics-ui/src/data/types.d.ts b/who-metrics-ui/src/data/types.d.ts deleted file mode 100644 index 251eb3e..0000000 --- a/who-metrics-ui/src/data/types.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -declare module "data.json" { - interface RepoData { - repoName: string; - collaboratorsCount: number; - projectsCount: number; - discussionsCount: number; - forksCount: number; - issuesCount: number; - openIssuesCount: number; - closedIssuesCount: number; - openPullRequestsCount: number; - mergedPullRequestsCount: number; - licenseName: string; - watchersCount: number; - } - - interface Data { - [key: string]: RepoData; - } - - const value: Data; - export default value; -} From 6d2c575335ebcbaa9f944a8ded6164f403febb98 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 9 Nov 2023 22:05:51 +0000 Subject: [PATCH 04/78] Use Primer component for RepositorySelector There seemed to be a bug with the Tremor selector where clearing the search filter by closing the popover didn't restore the filtered list of items. This commit switches to using the Primer `SelectPanel` instead. I added a `RepositorySelector` component to manage the state for this. There may have been a simpler fix - I'll probably take a look into the Tremor component to see if I'm missing something. We should decide what Component library we want to use for this - it might more sense to roll with Tremor if we're going to use that for the charting. I can also look into opening a PR upstream if there is in fact a bug in the Select component. Finally, I added some logic to make the Primer theme match the Tremor theme. This is a downside of using two different component libraries. --- .../src/components/DashboardExample.tsx | 13 ++++- .../src/components/RepositoriesTable.tsx | 22 ++----- .../src/components/RepositorySelector.tsx | 58 +++++++++++++++++++ 3 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 who-metrics-ui/src/components/RepositorySelector.tsx diff --git a/who-metrics-ui/src/components/DashboardExample.tsx b/who-metrics-ui/src/components/DashboardExample.tsx index 4fe87bb..786b104 100644 --- a/who-metrics-ui/src/components/DashboardExample.tsx +++ b/who-metrics-ui/src/components/DashboardExample.tsx @@ -13,7 +13,7 @@ import { DeltaType, } from "@tremor/react"; -import { Box } from "@primer/react"; +import { Box, useTheme as primerUseTheme } from "@primer/react"; import Image from "next/image"; import logo from "@/images/who-logo-wide.svg"; import { ChartView } from "./"; @@ -21,6 +21,7 @@ import { ChartView } from "./"; import KpiCard from "./KpiCard"; import RepositoriesTable from "./RepositoriesTable"; import Data from "../data/data.json"; +import { useTheme } from "next-themes"; type Kpi = { title: string; @@ -93,6 +94,16 @@ export const performance: DailyPerformance[] = [ ]; export const DashboardExample = () => { + const { theme, systemTheme } = useTheme(); + const { setColorMode } = primerUseTheme(); + if (theme === "light" || theme === "dark" || theme === "auto") { + setColorMode(theme); + } + + if (theme === "system" && systemTheme) { + setColorMode(systemTheme); + } + return (
{
- - {repos.map((item) => ( - - {item.repositoryName} - - ))} - + repo.repositoryName)} + /> - {licenses.map((license) => ( - - {license === "" ? "None" : license} - - ))} - -
- - - - {headers.map((label, index) => ( - - {label} - - ))} - - - - - {repos - .filter((repo) => isRepoSelected(repo)) - .map((repo) => ( - - {headers.map((header, index) => { - const property = Labels[header]; - const value = repo[property]; - return ( - - {value} - - ); - })} - - ))} - -
+ repo.repoName} + defaultColumnOptions={{ + sortable: true, + resizable: true, + }} + /> ); diff --git a/who-metrics-ui/src/pages/_app.tsx b/who-metrics-ui/src/pages/_app.tsx index 7c63f83..326e405 100644 --- a/who-metrics-ui/src/pages/_app.tsx +++ b/who-metrics-ui/src/pages/_app.tsx @@ -2,6 +2,8 @@ import "../styles/globals.css"; import type { AppProps } from "next/app"; import { ThemeProvider as NextThemeProvider } from "next-themes"; +import "react-data-grid/lib/styles.css"; + import { ThemeProvider as PrimerThemeProvider, BaseStyles, From 1eec1ec229d4a20354ba9dc1e23bc7ed0c2c6e31 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Thu, 7 Dec 2023 15:38:03 -0500 Subject: [PATCH 09/78] fix: add directory for data --- who-metrics-ui/src/data/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 who-metrics-ui/src/data/.gitignore diff --git a/who-metrics-ui/src/data/.gitignore b/who-metrics-ui/src/data/.gitignore new file mode 100644 index 0000000..40ddd6e --- /dev/null +++ b/who-metrics-ui/src/data/.gitignore @@ -0,0 +1,3 @@ +# This is used to create the datafile + +data.json From 5d180bc586425067fc7ca4c4e8ea6282385ecf90 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 7 Dec 2023 21:40:29 +0000 Subject: [PATCH 10/78] Update DataGrid - Adds sorting ability to the DataGrid component for all columns - Hides the ProjectsCount column for now. This one is technically private data so we should be careful before exposing it even though it seems relatively harmless - Removes the placeholder Overview component - Some small styling tweaks Co-authored-by: Andrew Henry Co-authored-by: lehcar (rachel) --- .../src/components/DashboardExample.tsx | 64 +---------- .../src/components/RepositoriesTable.tsx | 104 ++++++++++++++---- who-metrics-ui/tailwind.config.js | 9 ++ 3 files changed, 92 insertions(+), 85 deletions(-) diff --git a/who-metrics-ui/src/components/DashboardExample.tsx b/who-metrics-ui/src/components/DashboardExample.tsx index 786b104..90203c1 100644 --- a/who-metrics-ui/src/components/DashboardExample.tsx +++ b/who-metrics-ui/src/components/DashboardExample.tsx @@ -1,8 +1,6 @@ "use client"; import { - Card, - Grid, Title, Text, Tab, @@ -10,55 +8,16 @@ import { TabGroup, TabPanel, TabPanels, - DeltaType, } from "@tremor/react"; import { Box, useTheme as primerUseTheme } from "@primer/react"; import Image from "next/image"; import logo from "@/images/who-logo-wide.svg"; -import { ChartView } from "./"; -import KpiCard from "./KpiCard"; import RepositoriesTable from "./RepositoriesTable"; import Data from "../data/data.json"; import { useTheme } from "next-themes"; -type Kpi = { - title: string; - metric: string; - progress: number; - target: string; - delta: string; - deltaType: DeltaType; -}; - -const kpiData: Kpi[] = [ - { - title: "Sales", - metric: "$ 12,699", - progress: 15.9, - target: "$ 80,000", - delta: "13.2%", - deltaType: "moderateIncrease", - }, - { - title: "Profit", - metric: "$ 45,564", - progress: 36.5, - target: "$ 125,000", - delta: "23.9%", - deltaType: "increase", - }, - { - title: "Customers", - metric: "1,072", - progress: 53.6, - target: "2,000", - delta: "10.1%", - deltaType: "moderateDecrease", - }, -]; - export type DailyPerformance = { date: string; Sales: number; @@ -125,30 +84,9 @@ export const DashboardExample = () => { - Overview - Detail + Repositories - - - {kpiData.map((item) => ( - - ))} - -
- - - -
-
diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index fd26189..92e824f 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,10 +1,10 @@ import { InfoIcon } from "@primer/octicons-react"; import { Tooltip } from "@primer/react"; -import { Card, Flex, Title, Text } from "@tremor/react"; -import DataGrid from "react-data-grid"; +import { Flex, Text } from "@tremor/react"; +import DataGrid, { type SortColumn } from "react-data-grid"; import Data from "../data/data.json"; - +import { useState } from "react"; const repos = Object.values(Data["repositories"]); type Repo = (typeof repos)[0]; @@ -15,7 +15,6 @@ const Labels: Record = { Watchers: "watchersCount", "Open Issues": "openIssuesCount", "Closed Issues": "closedIssuesCount", - Projects: "projectsCount", "Open PRs": "openPullRequestsCount", "Merged PRs": "mergedPullRequestsCount", Forks: "forksCount", @@ -28,37 +27,98 @@ const DataGridColumns = Object.keys(Labels).map((label) => { }; }); +type Comparator = (a: Repo, b: Repo) => number; + +const getComparator = (sortColumn: keyof Repo): Comparator => { + switch (sortColumn) { + // number based sorting + case "closedIssuesCount": + case "collaboratorsCount": + case "discussionsCount": + case "forksCount": + case "issuesCount": + case "mergedPullRequestsCount": + case "openIssuesCount": + case "openPullRequestsCount": + case "projectsCount": + case "watchersCount": + return (a, b) => { + if (a[sortColumn] === b[sortColumn]) { + return 0; + } + + if (a[sortColumn] > b[sortColumn]) { + return 1; + } + + return -1; + }; + + // alphabetical sorting + case "licenseName": + case "repoName": + case "repositoryName": + return (a, b) => { + return a[sortColumn].localeCompare(b[sortColumn]); + }; + default: + throw new Error(`unsupported sortColumn: "${sortColumn}"`); + } +}; + const RepositoriesTable = () => { const subTitle = () => { return `${repos.length} total repositories`; }; + + const [sortColumns, setSortColumns] = useState([]); + + const sortedRepos = () => { + if (sortColumns.length === 0) return repos; + + const sortedRows = [...repos].sort((a, b) => { + for (const sort of sortColumns) { + const comparator = getComparator(sort.columnKey as keyof Repo); + const compResult = comparator(a, b); + if (compResult !== 0) { + return sort.direction === "ASC" ? compResult : -compResult; + } + } + return 0; + }); + + return sortedRows; + }; + return ( - - <> -
- - Repositories - - - - {subTitle()} - -
+
+
+ + + + + {subTitle()} + +
+
repo.repoName} defaultColumnOptions={{ sortable: true, resizable: true, }} + sortColumns={sortColumns} + onSortColumnsChange={setSortColumns} + style={{ height: "100%", width: "100%" }} /> - - +
+
); }; diff --git a/who-metrics-ui/tailwind.config.js b/who-metrics-ui/tailwind.config.js index b84e081..1cef135 100644 --- a/who-metrics-ui/tailwind.config.js +++ b/who-metrics-ui/tailwind.config.js @@ -10,6 +10,15 @@ module.exports = { transparent: 'transparent', current: 'currentColor', extend: { + minHeight: (theme) => ({ + ...theme('height'), + }), + height: { + 100: '24rem', + 120: '30rem', + 140: '36rem', + 160: '42rem', + }, colors: { // light mode tremor: { From 4a97d46460cdeda5afa03c1352277c23d2456ceb Mon Sep 17 00:00:00 2001 From: "lehcar (rachel)" <37482914+Lehcar@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:53:47 +0000 Subject: [PATCH 11/78] Add godotenv and load env variables in backend --- backend/cmd/main.go | 1 + backend/go.mod | 1 + backend/go.sum | 2 ++ 3 files changed, 4 insertions(+) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 86f3472..87bfd98 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,6 +5,7 @@ import ( "log" "os" + _ "github.com/joho/godotenv/autoload" "github.com/who-metrics/business/core" "github.com/who-metrics/business/core/collectors/github" "github.com/who-metrics/business/helpers" diff --git a/backend/go.mod b/backend/go.mod index 359a3f1..d0c3d8f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -9,6 +9,7 @@ require ( require ( github.com/golang/protobuf v1.5.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect golang.org/x/net v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/backend/go.sum b/backend/go.sum index 03af721..856b66e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,6 +4,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/shurcooL/githubv4 v0.0.0-20230305132112-efb623903184 h1:QwdHPs+b2raoqIDBgAkjYw89KHH2/CXbV+m2qrbDi9k= github.com/shurcooL/githubv4 v0.0.0-20230305132112-efb623903184/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc= From 7e6ac3040ddef34f72a819baaf336c5640ce35ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:25:11 +0000 Subject: [PATCH 12/78] Bump next from 13.2.4 to 13.5.1 in /who-metrics-ui Bumps [next](https://github.com/vercel/next.js) from 13.2.4 to 13.5.1. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v13.2.4...v13.5.1) --- updated-dependencies: - dependency-name: next dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- who-metrics-ui/package-lock.json | 229 +++++++++++++------------------ who-metrics-ui/package.json | 2 +- 2 files changed, 99 insertions(+), 132 deletions(-) diff --git a/who-metrics-ui/package-lock.json b/who-metrics-ui/package-lock.json index 95cfad5..5ac92b4 100644 --- a/who-metrics-ui/package-lock.json +++ b/who-metrics-ui/package-lock.json @@ -24,7 +24,7 @@ "js-cookie": "^3.0.1", "kysely": "^0.23.4", "kysely-planetscale": "^1.3.0", - "next": "^13.2.0", + "next": "^13.5.1", "next-auth": "^4.19.2", "next-themes": "^0.2.1", "postcss": "^8.4.31", @@ -773,9 +773,9 @@ "dev": true }, "node_modules/@next/env": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.2.4.tgz", - "integrity": "sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==" + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.1.tgz", + "integrity": "sha512-CIMWiOTyflFn/GFx33iYXkgLSQsMQZV4jB91qaj/TfxGaGOXxn8C1j72TaUSPIyN7ziS/AYG46kGmnvuk1oOpg==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.2.4", @@ -785,40 +785,10 @@ "glob": "7.1.7" } }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz", - "integrity": "sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz", - "integrity": "sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz", - "integrity": "sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.1.tgz", + "integrity": "sha512-Bcd0VFrLHZnMmJy6LqV1CydZ7lYaBao8YBEdQUVzV8Ypn/l5s//j5ffjfvMzpEQ4mzlAj3fIY+Bmd9NxpWhACw==", "cpu": [ "arm64" ], @@ -831,9 +801,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz", - "integrity": "sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.1.tgz", + "integrity": "sha512-uvTZrZa4D0bdWa1jJ7X1tBGIxzpqSnw/ATxWvoRO9CVBvXSx87JyuISY+BWsfLFF59IRodESdeZwkWM2l6+Kjg==", "cpu": [ "x64" ], @@ -845,40 +815,10 @@ "node": ">= 10" } }, - "node_modules/@next/swc-freebsd-x64": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz", - "integrity": "sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz", - "integrity": "sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz", - "integrity": "sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.1.tgz", + "integrity": "sha512-/52ThlqdORPQt3+AlMoO+omicdYyUEDeRDGPAj86ULpV4dg+/GCFCKAmFWT0Q4zChFwsAoZUECLcKbRdcc0SNg==", "cpu": [ "arm64" ], @@ -891,9 +831,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz", - "integrity": "sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.1.tgz", + "integrity": "sha512-L4qNXSOHeu1hEAeeNsBgIYVnvm0gg9fj2O2Yx/qawgQEGuFBfcKqlmIE/Vp8z6gwlppxz5d7v6pmHs1NB6R37w==", "cpu": [ "arm64" ], @@ -906,9 +846,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz", - "integrity": "sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.1.tgz", + "integrity": "sha512-QVvMrlrFFYvLtABk092kcZ5Mzlmsk2+SV3xYuAu8sbTuIoh0U2+HGNhVklmuYCuM3DAAxdiMQTNlRQmNH11udw==", "cpu": [ "x64" ], @@ -921,9 +861,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz", - "integrity": "sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.1.tgz", + "integrity": "sha512-bBnr+XuWc28r9e8gQ35XBtyi5KLHLhTbEvrSgcWna8atI48sNggjIK8IyiEBO3KIrcUVXYkldAzGXPEYMnKt1g==", "cpu": [ "x64" ], @@ -936,9 +876,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz", - "integrity": "sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.1.tgz", + "integrity": "sha512-EQGeE4S5c9v06jje9gr4UlxqUEA+zrsgPi6kg9VwR+dQHirzbnVJISF69UfKVkmLntknZJJI9XpWPB6q0Z7mTg==", "cpu": [ "arm64" ], @@ -951,9 +891,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz", - "integrity": "sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.1.tgz", + "integrity": "sha512-1y31Q6awzofVjmbTLtRl92OX3s+W0ZfO8AP8fTnITcIo9a6ATDc/eqa08fd6tSpFu6IFpxOBbdevOjwYTGx/AQ==", "cpu": [ "ia32" ], @@ -966,9 +906,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz", - "integrity": "sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.1.tgz", + "integrity": "sha512-+9XBQizy7X/GuwNegq+5QkkxAPV7SBsIwapVRQd9WSvvU20YO23B3bZUpevdabi4fsd25y9RJDDncljy/V54ww==", "cpu": [ "x64" ], @@ -1132,14 +1072,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-aria/ssr/node_modules/@swc/helpers": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", - "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -1264,9 +1196,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", - "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", "dependencies": { "tslib": "^2.4.0" } @@ -2006,6 +1938,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3516,6 +3459,11 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "node_modules/globals": { "version": "13.20.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", @@ -4382,41 +4330,38 @@ "dev": true }, "node_modules/next": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-13.2.4.tgz", - "integrity": "sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.1.tgz", + "integrity": "sha512-GIudNR7ggGUZoIL79mSZcxbXK9f5pwAIPZxEM8+j2yLqv5RODg4TkmUlaKSYVqE1bPQueamXSqdC3j7axiTSEg==", "dependencies": { - "@next/env": "13.2.4", - "@swc/helpers": "0.4.14", + "@next/env": "13.5.1", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", - "styled-jsx": "5.1.1" + "styled-jsx": "5.1.1", + "watchpack": "2.4.0", + "zod": "3.21.4" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=14.6.0" + "node": ">=16.14.0" }, "optionalDependencies": { - "@next/swc-android-arm-eabi": "13.2.4", - "@next/swc-android-arm64": "13.2.4", - "@next/swc-darwin-arm64": "13.2.4", - "@next/swc-darwin-x64": "13.2.4", - "@next/swc-freebsd-x64": "13.2.4", - "@next/swc-linux-arm-gnueabihf": "13.2.4", - "@next/swc-linux-arm64-gnu": "13.2.4", - "@next/swc-linux-arm64-musl": "13.2.4", - "@next/swc-linux-x64-gnu": "13.2.4", - "@next/swc-linux-x64-musl": "13.2.4", - "@next/swc-win32-arm64-msvc": "13.2.4", - "@next/swc-win32-ia32-msvc": "13.2.4", - "@next/swc-win32-x64-msvc": "13.2.4" + "@next/swc-darwin-arm64": "13.5.1", + "@next/swc-darwin-x64": "13.5.1", + "@next/swc-linux-arm64-gnu": "13.5.1", + "@next/swc-linux-arm64-musl": "13.5.1", + "@next/swc-linux-x64-gnu": "13.5.1", + "@next/swc-linux-x64-musl": "13.5.1", + "@next/swc-win32-arm64-msvc": "13.5.1", + "@next/swc-win32-ia32-msvc": "13.5.1", + "@next/swc-win32-x64-msvc": "13.5.1" }, "peerDependencies": { - "@opentelemetry/api": "^1.4.0", - "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", + "@opentelemetry/api": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -4425,12 +4370,6 @@ "@opentelemetry/api": { "optional": true }, - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, "sass": { "optional": true } @@ -5393,6 +5332,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -5982,6 +5929,18 @@ "d3-timer": "^3.0.1" } }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6072,6 +6031,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/who-metrics-ui/package.json b/who-metrics-ui/package.json index f56d148..ee185c8 100644 --- a/who-metrics-ui/package.json +++ b/who-metrics-ui/package.json @@ -25,7 +25,7 @@ "js-cookie": "^3.0.1", "kysely": "^0.23.4", "kysely-planetscale": "^1.3.0", - "next": "^13.2.0", + "next": "^13.5.1", "next-auth": "^4.19.2", "next-themes": "^0.2.1", "postcss": "^8.4.31", From 0d42cda5674e39508207cddc5549756fa771fb30 Mon Sep 17 00:00:00 2001 From: Diego Arostegui Date: Sun, 10 Dec 2023 00:10:19 +0100 Subject: [PATCH 13/78] Update README.md Fixing Team table typos and distribution --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ae09a59..c2b39c6 100644 --- a/README.md +++ b/README.md @@ -13,47 +13,47 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss ## Team - + @@ -84,4 +84,4 @@ make build ./backend/bin/metrics ``` -This will generate a new `data.json` file in the UI directory which can be imported directly as part of the static build. \ No newline at end of file +This will generate a new `data.json` file in the UI directory which can be imported directly as part of the static build. From c94764132cefa865c03f6d6c5aba263fac3d6b50 Mon Sep 17 00:00:00 2001 From: Diego Arostegui Date: Sun, 10 Dec 2023 00:22:56 +0100 Subject: [PATCH 14/78] Update README.md --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c2b39c6..86b45a8 100644 --- a/README.md +++ b/README.md @@ -10,47 +10,46 @@ Monitor open source health across different projects to have a high level view t Issue Project [here](https://github.com/github/SI-skills-based-volunteering/issues/147#issuecomment-1472370039) - ## Team
-

+
@liliana3186
- Liliana Torres
+ Liliana Torres
Project Lead, Senior Data Analyst
-
+
@hasan-dot
- Hassan Hawache
- Sofware Engineer III + Hassan Hawache
+ Sofware Engineer III

- @daristegui
- Diego Arostegui
+ @darostegui
+ Diego Arostegui
Customer Reliability Engineer III

@ajhenry
- Andrew Henry
+ Andrew Henry
Senior Software Engineer

@joannaakl
- Joanna Krzek-Lubowiecka
+ Joanna Krzek-Lubowiecka
Software Engineer III
-
+
@dmgardiner25
- David Gardiner
+ David Gardiner
Software Engineer II

@ipc103
- Ian Candy
+ Ian Candy
Senior Software Engineer
- - - - - - -
+
@liliana3186
Liliana Torres
Project Lead, Senior Data Analyst
+
@hasan-dot
Hassan Hawache
Sofware Engineer III
+
@darostegui
Diego Arostegui
Customer Reliability Engineer III
+
@ajhenry
Andrew Henry
Senior Software Engineer
+
@joannaakl
Joanna Krzek-Lubowiecka
Software Engineer III
+
@dmgardiner25
David Gardiner
Software Engineer II
+
@ipc103
Ian Candy
@@ -61,9 +60,6 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss - - - ## Development in Codespaces ### UI From 920295568c7f0ea7f36deba677ebc8dd4bd0f4c6 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 11 Dec 2023 22:47:10 +0000 Subject: [PATCH 15/78] chore: ignore prettier rules if installed --- who-metrics-ui/.eslintrc.js | 1 + who-metrics-ui/.prettierrc | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 who-metrics-ui/.prettierrc diff --git a/who-metrics-ui/.eslintrc.js b/who-metrics-ui/.eslintrc.js index 14d7402..926ee62 100644 --- a/who-metrics-ui/.eslintrc.js +++ b/who-metrics-ui/.eslintrc.js @@ -63,6 +63,7 @@ const baseConfig = { }, ignorePatterns: ["*__generated__*"], rules: { + "prettier/prettier": 0, // Sorry but prettier/prettier is quite problematic "import/no-unresolved": [ "error", { diff --git a/who-metrics-ui/.prettierrc b/who-metrics-ui/.prettierrc new file mode 100644 index 0000000..6d99a58 --- /dev/null +++ b/who-metrics-ui/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2 +} From 9cd66767d07a2227ad73fd8cd4d5ff2fb75d4d86 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 11 Dec 2023 22:47:18 +0000 Subject: [PATCH 16/78] feat: add repo headers --- .../src/components/RepositoriesTable.tsx | 138 +++++++++++++----- 1 file changed, 104 insertions(+), 34 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 92e824f..9bfb86e 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,31 +1,49 @@ import { InfoIcon } from "@primer/octicons-react"; import { Tooltip } from "@primer/react"; import { Flex, Text } from "@tremor/react"; -import DataGrid, { type SortColumn } from "react-data-grid"; +import DataGrid, { Column, type RenderHeaderCellProps, type SortColumn } from "react-data-grid"; +import { createContext, useContext, useState } from "react"; import Data from "../data/data.json"; -import { useState } from "react"; const repos = Object.values(Data["repositories"]); type Repo = (typeof repos)[0]; -const Labels: Record = { - Name: "repositoryName", - Collaborators: "collaboratorsCount", - License: "licenseName", - Watchers: "watchersCount", - "Open Issues": "openIssuesCount", - "Closed Issues": "closedIssuesCount", - "Open PRs": "openPullRequestsCount", - "Merged PRs": "mergedPullRequestsCount", - Forks: "forksCount", -} as const; - -const DataGridColumns = Object.keys(Labels).map((label) => { - return { - key: Labels[label], - name: label, - }; -}); +function inputStopPropagation(event: React.KeyboardEvent) { + event.stopPropagation(); +} + +function selectStopPropagation(event: React.KeyboardEvent) { + if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) { + event.stopPropagation(); + } +} + + +type Filter = Repo + +function FilterRenderer({ + tabIndex, + column, + children +}: RenderHeaderCellProps & { + children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; +}) { + const filters = useContext(FilterContext)!; + return ( +
+
{column.name}
+ Hello +
{children({ tabIndex, filters })}
+
+ ); +} + +// Context is needed to read filter values otherwise columns are +// re-created when filters are changed and filter loses focus +const FilterContext = createContext(undefined); + +type Row = Record + type Comparator = (a: Repo, b: Repo) => number; @@ -67,10 +85,61 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { }; const RepositoriesTable = () => { + const [globalFilters, setGlobalFilters] = useState( + {} as Filter + ); + + + // This needs a type, technically it's a Column but needs to be typed + const labels: Record> = { + Name: { + key: "repositoryName", + name: "Name", + headerCellClass: "h-32", + renderHeaderCell: (p) => { + console.log(p) + return {...(p as any)}> + {({ filters, ...rest }) => ( + + setGlobalFilters((otherFilters) => ({ + ...otherFilters, + repositoryName: e.target.value + })) + } + onKeyDown={inputStopPropagation} + onClick={e => e.stopPropagation()} + /> + )} + + } + }, + // Collaborators: "collaboratorsCount", + // License: "licenseName", + // Watchers: "watchersCount", + // "Open Issues": "openIssuesCount", + // "Closed Issues": "closedIssuesCount", + // "Open PRs": "openPullRequestsCount", + // "Merged PRs": "mergedPullRequestsCount", + // Forks: "forksCount", + } as const; + + + const dataGridColumns = Object.entries(labels).map(([_, columnProps]) => columnProps); + const subTitle = () => { return `${repos.length} total repositories`; }; + // This selects a field to populate a dropdown with + const dropdownOptions = (field: keyof Repo) => + Array.from(new Set(repos.map((r) => r[field]))).map((d) => ({ + label: d, + value: d, + })); + const [sortColumns, setSortColumns] = useState([]); const sortedRepos = () => { @@ -104,20 +173,21 @@ const RepositoriesTable = () => { {subTitle()} -
- repo.repoName} - defaultColumnOptions={{ - sortable: true, - resizable: true, - }} - sortColumns={sortColumns} - onSortColumnsChange={setSortColumns} - style={{ height: "100%", width: "100%" }} - /> -
+ +
+ repo.repoName} + defaultColumnOptions={{ + sortable: true, + resizable: true, + }} + sortColumns={sortColumns} + onSortColumnsChange={setSortColumns} + style={{ height: "100%", width: "100%" }} + /> +
); }; From f93351200f09003ccb034fea72457f4f8a749991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 22:52:14 +0000 Subject: [PATCH 17/78] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 965a09a..9a3e2dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,13 +22,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" From 6dedf33a3b7847707af121eb8d13fd849f638d5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 22:37:15 +0000 Subject: [PATCH 18/78] Bump actions/upload-pages-artifact from 2 to 3 Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nextjs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index a21d9dd..06a4968 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -104,7 +104,7 @@ jobs: - name: Static HTML export with Next.js run: cd "${{ github.workspace }}/who-metrics-ui" && ${{ steps.detect-package-manager.outputs.runner }} next export - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: "${{ github.workspace }}/who-metrics-ui/out" From 8ed884b447da93828ca044de7aa1cb922e33217a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 22:37:18 +0000 Subject: [PATCH 19/78] Bump actions/deploy-pages from 2 to 4 Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 2 to 4. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nextjs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index a21d9dd..0a858ea 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -118,4 +118,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From 1ff3f9edfb86d2002907938c3b660610e5cbe43b Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Fri, 12 Jan 2024 14:16:15 -0500 Subject: [PATCH 20/78] feat: update headers --- who-metrics-ui/.vscode/launch.json | 28 ++++ ...boardExample.tsx => OrganizationSheet.tsx} | 54 +------ .../src/components/RepositoriesTable.tsx | 145 ++++++++++-------- who-metrics-ui/src/components/index.ts | 4 +- who-metrics-ui/src/pages/index.tsx | 4 +- 5 files changed, 118 insertions(+), 117 deletions(-) create mode 100644 who-metrics-ui/.vscode/launch.json rename who-metrics-ui/src/components/{DashboardExample.tsx => OrganizationSheet.tsx} (62%) diff --git a/who-metrics-ui/.vscode/launch.json b/who-metrics-ui/.vscode/launch.json new file mode 100644 index 0000000..ee3bdd7 --- /dev/null +++ b/who-metrics-ui/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + } + ] +} diff --git a/who-metrics-ui/src/components/DashboardExample.tsx b/who-metrics-ui/src/components/OrganizationSheet.tsx similarity index 62% rename from who-metrics-ui/src/components/DashboardExample.tsx rename to who-metrics-ui/src/components/OrganizationSheet.tsx index 90203c1..1a1ade6 100644 --- a/who-metrics-ui/src/components/DashboardExample.tsx +++ b/who-metrics-ui/src/components/OrganizationSheet.tsx @@ -1,58 +1,20 @@ "use client"; import { - Title, - Text, - Tab, - TabList, - TabGroup, - TabPanel, - TabPanels, + Tab, TabGroup, TabList, TabPanel, + TabPanels, Text, Title } from "@tremor/react"; +import logo from "@/images/who-logo-wide.svg"; import { Box, useTheme as primerUseTheme } from "@primer/react"; import Image from "next/image"; -import logo from "@/images/who-logo-wide.svg"; -import RepositoriesTable from "./RepositoriesTable"; -import Data from "../data/data.json"; import { useTheme } from "next-themes"; +import data from "../data/data.json"; +import RepositoriesTable from "./RepositoriesTable"; -export type DailyPerformance = { - date: string; - Sales: number; - Profit: number; - Customers: number; -}; - -export const performance: DailyPerformance[] = [ - { - date: "2023-05-01", - Sales: 900.73, - Profit: 173, - Customers: 73, - }, - { - date: "2023-05-02", - Sales: 1000.74, - Profit: 174.6, - Customers: 74, - }, - { - date: "2023-05-03", - Sales: 1100.93, - Profit: 293.1, - Customers: 293, - }, - { - date: "2023-05-04", - Sales: 1200.9, - Profit: 290.2, - Customers: 29, - }, -]; -export const DashboardExample = () => { +export const OrganizationSheet = () => { const { theme, systemTheme } = useTheme(); const { setColorMode } = primerUseTheme(); if (theme === "light" || theme === "dark" || theme === "auto") { @@ -76,11 +38,11 @@ export const DashboardExample = () => { width={150} alt="World Health Organization logo" /> - {Data.orgInfo.name} Open Source Dashboard + {data.orgInfo.name} Open Source Dashboard This project includes metrics about the Open Source repositories for the - {Data.orgInfo.name}. + {data.orgInfo.name}. diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 9bfb86e..3cfec25 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,11 +1,11 @@ -import { InfoIcon } from "@primer/octicons-react"; -import { Tooltip } from "@primer/react"; -import { Flex, Text } from "@tremor/react"; -import DataGrid, { Column, type RenderHeaderCellProps, type SortColumn } from "react-data-grid"; - -import { createContext, useContext, useState } from "react"; -import Data from "../data/data.json"; -const repos = Object.values(Data["repositories"]); +import { InfoIcon } from '@primer/octicons-react'; +import { Tooltip } from '@primer/react'; +import { Flex, Text } from '@tremor/react'; +import DataGrid, { Column, type RenderHeaderCellProps, type SortColumn } from 'react-data-grid'; + +import { createContext, useCallback, useContext, useState } from 'react'; +import Data from '../data/data.json'; +const repos = Object.values(Data['repositories']); type Repo = (typeof repos)[0]; function inputStopPropagation(event: React.KeyboardEvent) { @@ -18,19 +18,20 @@ function selectStopPropagation(event: React.KeyboardEvent) { } } - -type Filter = Repo +type Filter = { + repositoryName?: string; +}; function FilterRenderer({ tabIndex, column, - children + children, }: RenderHeaderCellProps & { children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; }) { const filters = useContext(FilterContext)!; return ( -
+
{column.name}
Hello
{children({ tabIndex, filters })}
@@ -42,24 +43,23 @@ function FilterRenderer({ // re-created when filters are changed and filter loses focus const FilterContext = createContext(undefined); -type Row = Record - +type Row = Record; type Comparator = (a: Repo, b: Repo) => number; const getComparator = (sortColumn: keyof Repo): Comparator => { switch (sortColumn) { // number based sorting - case "closedIssuesCount": - case "collaboratorsCount": - case "discussionsCount": - case "forksCount": - case "issuesCount": - case "mergedPullRequestsCount": - case "openIssuesCount": - case "openPullRequestsCount": - case "projectsCount": - case "watchersCount": + case 'closedIssuesCount': + case 'collaboratorsCount': + case 'discussionsCount': + case 'forksCount': + case 'issuesCount': + case 'mergedPullRequestsCount': + case 'openIssuesCount': + case 'openPullRequestsCount': + case 'projectsCount': + case 'watchersCount': return (a, b) => { if (a[sortColumn] === b[sortColumn]) { return 0; @@ -73,11 +73,11 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { }; // alphabetical sorting - case "licenseName": - case "repoName": - case "repositoryName": + case 'licenseName': + case 'repoName': + case 'repositoryName': return (a, b) => { - return a[sortColumn].localeCompare(b[sortColumn]); + return a[sortColumn].toLowerCase().localeCompare(b[sortColumn].toLowerCase()); }; default: throw new Error(`unsupported sortColumn: "${sortColumn}"`); @@ -85,36 +85,37 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { }; const RepositoriesTable = () => { - const [globalFilters, setGlobalFilters] = useState( - {} as Filter - ); - + const [globalFilters, setGlobalFilters] = useState({ + repositoryName: undefined, + } as Filter); // This needs a type, technically it's a Column but needs to be typed const labels: Record> = { Name: { - key: "repositoryName", - name: "Name", - headerCellClass: "h-32", - renderHeaderCell: (p) => { - console.log(p) - return {...(p as any)}> - {({ filters, ...rest }) => ( - - setGlobalFilters((otherFilters) => ({ - ...otherFilters, - repositoryName: e.target.value - })) - } - onKeyDown={inputStopPropagation} - onClick={e => e.stopPropagation()} - /> - )} - - } + key: 'repositoryName', + name: 'Name', + headerCellClass: 'h-32', + // renderHeaderCell: (p) => { + // console.log(p); + // return ( + // {...(p as any)}> + // {({ filters, ...rest }) => ( + // + // setGlobalFilters((otherFilters) => ({ + // ...otherFilters, + // repositoryName: e.target.value, + // })) + // } + // onKeyDown={inputStopPropagation} + // onClick={(e) => e.stopPropagation()} + // /> + // )} + // + // ); + // }, }, // Collaborators: "collaboratorsCount", // License: "licenseName", @@ -126,7 +127,6 @@ const RepositoriesTable = () => { // Forks: "forksCount", } as const; - const dataGridColumns = Object.entries(labels).map(([_, columnProps]) => columnProps); const subTitle = () => { @@ -142,15 +142,17 @@ const RepositoriesTable = () => { const [sortColumns, setSortColumns] = useState([]); - const sortedRepos = () => { - if (sortColumns.length === 0) return repos; + const sortRepos = (inputRepos: Repo[]) => { + if (sortColumns.length === 0) { + return repos; + } - const sortedRows = [...repos].sort((a, b) => { + const sortedRows = [...inputRepos].sort((a, b) => { for (const sort of sortColumns) { const comparator = getComparator(sort.columnKey as keyof Repo); const compResult = comparator(a, b); if (compResult !== 0) { - return sort.direction === "ASC" ? compResult : -compResult; + return sort.direction === 'ASC' ? compResult : -compResult; } } return 0; @@ -159,14 +161,21 @@ const RepositoriesTable = () => { return sortedRows; }; + const filterRepos = useCallback( + (inputRepos: Repo[]) => { + const result = inputRepos.filter((repo) => { + return globalFilters.repositoryName ? repo.repositoryName.includes(globalFilters.repositoryName) : true; + }); + + return result; + }, + [globalFilters], + ); + return (
- + @@ -176,8 +185,9 @@ const RepositoriesTable = () => {
repo.repoName} defaultColumnOptions={{ sortable: true, @@ -185,9 +195,10 @@ const RepositoriesTable = () => { }} sortColumns={sortColumns} onSortColumnsChange={setSortColumns} - style={{ height: "100%", width: "100%" }} + style={{ height: '100%', width: '100%' }} /> -
+
+
); }; diff --git a/who-metrics-ui/src/components/index.ts b/who-metrics-ui/src/components/index.ts index 9b09fe5..bdd9366 100644 --- a/who-metrics-ui/src/components/index.ts +++ b/who-metrics-ui/src/components/index.ts @@ -1,2 +1,2 @@ -export { ChartView } from "./ChartView"; -export { DashboardExample } from "./DashboardExample"; +export { ChartView } from './ChartView'; +export { OrganizationSheet } from './OrganizationSheet'; diff --git a/who-metrics-ui/src/pages/index.tsx b/who-metrics-ui/src/pages/index.tsx index e8568d0..2374d8e 100644 --- a/who-metrics-ui/src/pages/index.tsx +++ b/who-metrics-ui/src/pages/index.tsx @@ -1,10 +1,10 @@ /* eslint-disable filenames/match-regex */ -import { DashboardExample } from "../components"; +import { OrganizationSheet } from "../components/OrganizationSheet"; export default function PlaygroundPage() { return (
- +
); } From aea388cd7a728242de91b3006c0aac085bb4d244 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Fri, 12 Jan 2024 14:53:00 -0500 Subject: [PATCH 21/78] feat: add header for license type --- .../src/components/RepositoriesTable.tsx | 129 +++++++++++++----- 1 file changed, 98 insertions(+), 31 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 3cfec25..aeded45 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -20,31 +20,41 @@ function selectStopPropagation(event: React.KeyboardEvent) { type Filter = { repositoryName?: string; + licenseName?: string[]; }; -function FilterRenderer({ +/** + * Wrapper for rendering column header cell + * @param { + * tabIndex: number; + * column: Column; + * children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; + * } props + * @returns + */ +const FilterRenderer = ({ tabIndex, column, - children, + children: filterFunction, + sortDirection, }: RenderHeaderCellProps & { children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; -}) { +}) => { const filters = useContext(FilterContext)!; + return (
{column.name}
- Hello -
{children({ tabIndex, filters })}
+
{sortDirection === 'ASC' ? 'UP' : sortDirection === 'DESC' ? 'DOWN' : null}
+
{filterFunction({ tabIndex, filters })}
); -} +}; // Context is needed to read filter values otherwise columns are // re-created when filters are changed and filter loses focus const FilterContext = createContext(undefined); -type Row = Record; - type Comparator = (a: Repo, b: Repo) => number; const getComparator = (sortColumn: keyof Repo): Comparator => { @@ -87,6 +97,7 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { const RepositoriesTable = () => { const [globalFilters, setGlobalFilters] = useState({ repositoryName: undefined, + licenseName: [], } as Filter); // This needs a type, technically it's a Column but needs to be typed @@ -95,30 +106,74 @@ const RepositoriesTable = () => { key: 'repositoryName', name: 'Name', headerCellClass: 'h-32', - // renderHeaderCell: (p) => { - // console.log(p); - // return ( - // {...(p as any)}> - // {({ filters, ...rest }) => ( - // - // setGlobalFilters((otherFilters) => ({ - // ...otherFilters, - // repositoryName: e.target.value, - // })) - // } - // onKeyDown={inputStopPropagation} - // onClick={(e) => e.stopPropagation()} - // /> - // )} - // - // ); - // }, + renderHeaderCell: (p) => { + return ( + {...p}> + {({ filters, ...rest }) => ( + + setGlobalFilters((otherFilters) => ({ + ...otherFilters, + repositoryName: e.target.value, + })) + } + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} + /> + )} + + ); + }, + }, + License: { + key: 'licenseName', + name: 'License', + headerCellClass: 'h-32', + renderHeaderCell: (p) => { + return ( + {...p}> + {({ filters, ...rest }) => ( + + )} + + ); + }, }, // Collaborators: "collaboratorsCount", - // License: "licenseName", // Watchers: "watchersCount", // "Open Issues": "openIssuesCount", // "Closed Issues": "closedIssuesCount", @@ -161,10 +216,22 @@ const RepositoriesTable = () => { return sortedRows; }; + /** + * Uses globalFilters to filter the repos that are then passed to sortRepos + * + * NOTE: We use some hacks like addings 'all' to the licenseName filter to + * make it easier to filter the repos. + */ const filterRepos = useCallback( (inputRepos: Repo[]) => { const result = inputRepos.filter((repo) => { - return globalFilters.repositoryName ? repo.repositoryName.includes(globalFilters.repositoryName) : true; + return ( + (globalFilters.repositoryName ? repo.repositoryName.includes(globalFilters.repositoryName) : true) && + ((globalFilters.licenseName?.length ?? 0 > 0 + ? globalFilters.licenseName?.includes(repo.licenseName) + : true) || + globalFilters.licenseName?.includes('all')) + ); }); return result; From 5e4921c0371ba29de146af58c8a8d98be271d27a Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 16 Jan 2024 16:05:52 -0500 Subject: [PATCH 22/78] feat: add collaborator count filter --- .../src/components/RepositoriesTable.tsx | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index aeded45..0dfa5af 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -21,6 +21,7 @@ function selectStopPropagation(event: React.KeyboardEvent) { type Filter = { repositoryName?: string; licenseName?: string[]; + collaboratorsCount?: Array; }; /** @@ -173,7 +174,52 @@ const RepositoriesTable = () => { ); }, }, - // Collaborators: "collaboratorsCount", + Collaborators: { + key: 'collaboratorsCount', + name: 'Collaborator Count', + headerCellClass: 'h-32', + renderHeaderCell: (p) => { + return ( + {...p}> + {({ filters, ...rest }) => ( +
+ + { + console.log(e.target.value); + setGlobalFilters((otherFilters) => ({ + ...otherFilters, + collaboratorsCount: [Number(e.target.value), otherFilters.collaboratorsCount?.[1]], + })); + }} + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} + /> + + + setGlobalFilters((otherFilters) => ({ + ...otherFilters, + collaboratorsCount: [0, Number(e.target.value)], + })) + } + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} + /> +
+ )} + + ); + }, + }, // Watchers: "watchersCount", // "Open Issues": "openIssuesCount", // "Closed Issues": "closedIssuesCount", @@ -230,7 +276,11 @@ const RepositoriesTable = () => { ((globalFilters.licenseName?.length ?? 0 > 0 ? globalFilters.licenseName?.includes(repo.licenseName) : true) || - globalFilters.licenseName?.includes('all')) + globalFilters.licenseName?.includes('all')) && + (globalFilters.collaboratorsCount + ? (globalFilters.collaboratorsCount?.[0] ?? 0) <= repo.collaboratorsCount && + repo.collaboratorsCount <= (globalFilters.collaboratorsCount[1] ?? Infinity) + : true) ); }); From 3d2376598823bf41095e9f99a86a72f01eb73bd2 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Mon, 22 Jan 2024 15:47:21 -0500 Subject: [PATCH 23/78] feat: add header popovers to all columns --- who-metrics-ui/package-lock.json | 10 + who-metrics-ui/package.json | 1 + .../src/components/RepositoriesTable.tsx | 270 ++++++++++++++---- 3 files changed, 228 insertions(+), 53 deletions(-) diff --git a/who-metrics-ui/package-lock.json b/who-metrics-ui/package-lock.json index 0d30abe..f137533 100644 --- a/who-metrics-ui/package-lock.json +++ b/who-metrics-ui/package-lock.json @@ -33,6 +33,7 @@ "react": "^18.2.0", "react-data-grid": "^7.0.0-beta.41", "react-dom": "^18.2.0", + "react-tiny-popover": "^8.0.4", "server-only": "^0.0.1", "styled-components": "^5.3.11", "tailwindcss": "^3.2.7", @@ -5140,6 +5141,15 @@ "react-dom": ">=15.0.0" } }, + "node_modules/react-tiny-popover": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-tiny-popover/-/react-tiny-popover-8.0.4.tgz", + "integrity": "sha512-pn0Y/G0gyMdYTBEWSKCCnaZsXAa54PkfnRE4fnMM5633SSClYrXxwXKc6vPYgJ9shLatGginxMjnhXq6guZmng==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/who-metrics-ui/package.json b/who-metrics-ui/package.json index 7f56a84..14a08be 100644 --- a/who-metrics-ui/package.json +++ b/who-metrics-ui/package.json @@ -34,6 +34,7 @@ "react": "^18.2.0", "react-data-grid": "^7.0.0-beta.41", "react-dom": "^18.2.0", + "react-tiny-popover": "^8.0.4", "server-only": "^0.0.1", "styled-components": "^5.3.11", "tailwindcss": "^3.2.7", diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 0dfa5af..80bdab6 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,9 +1,15 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable primer-react/no-system-props */ +/* eslint-disable react/jsx-no-comment-textnodes */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable react/no-unescaped-entities */ import { InfoIcon } from '@primer/octicons-react'; -import { Tooltip } from '@primer/react'; +import { Button, Tooltip } from '@primer/react'; import { Flex, Text } from '@tremor/react'; import DataGrid, { Column, type RenderHeaderCellProps, type SortColumn } from 'react-data-grid'; +import { Popover } from 'react-tiny-popover'; -import { createContext, useCallback, useContext, useState } from 'react'; +import { createContext, FC, useCallback, useContext, useRef, useState } from 'react'; import Data from '../data/data.json'; const repos = Object.values(Data['repositories']); type Repo = (typeof repos)[0]; @@ -22,6 +28,58 @@ type Filter = { repositoryName?: string; licenseName?: string[]; collaboratorsCount?: Array; + watchersCount?: Array; + openIssuesCount?: Array; + openPullRequestsCount?: Array; + closedIssuesCount?: Array; + mergedPullRequestsCount?: Array; + forksCount?: Array; +}; + +const MinMaxRenderer: FC<{ + headerCellProps: RenderHeaderCellProps; + filters: Filter; + updateFilters: ((filters: Filter) => void) & ((filters: (filters: Filter) => Filter) => void); + filterName: keyof Filter; +}> = ({ headerCellProps, filters, updateFilters, filterName }) => { + return ( + {...headerCellProps}> + {({ ...rest }) => ( +
+ + { + updateFilters((globalFilters) => ({ + ...globalFilters, + [filterName]: [Number(e.target.value), globalFilters[filterName]?.[1]], + })); + }} + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} + /> + + + updateFilters({ + ...filters, + [filterName]: [0, Number(e.target.value)], + }) + } + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} + /> +
+ )} + + ); }; /** @@ -42,12 +100,42 @@ const FilterRenderer = ({ children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; }) => { const filters = useContext(FilterContext)!; + const clickMeButtonRef = useRef(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); return ( -
+
{column.name}
-
{sortDirection === 'ASC' ? 'UP' : sortDirection === 'DESC' ? 'DOWN' : null}
-
{filterFunction({ tabIndex, filters })}
+
+ {sortDirection === 'ASC' ? 'UP' : sortDirection === 'DESC' ? 'DOWN' : null} + setIsPopoverOpen(false)} + ref={clickMeButtonRef} // if you'd like a ref to your popover's child, you can grab one here + content={() => ( +
e.stopPropagation()}> +
+

Filter {column.name}

+ {filterFunction({ tabIndex, filters })} +
+
+ )} + > + +
+
); }; @@ -106,7 +194,7 @@ const RepositoriesTable = () => { Name: { key: 'repositoryName', name: 'Name', - headerCellClass: 'h-32', + renderHeaderCell: (p) => { return ( {...p}> @@ -131,7 +219,7 @@ const RepositoriesTable = () => { License: { key: 'licenseName', name: 'License', - headerCellClass: 'h-32', + renderHeaderCell: (p) => { return ( {...p}> @@ -177,55 +265,107 @@ const RepositoriesTable = () => { Collaborators: { key: 'collaboratorsCount', name: 'Collaborator Count', - headerCellClass: 'h-32', renderHeaderCell: (p) => { return ( - {...p}> - {({ filters, ...rest }) => ( -
- - { - console.log(e.target.value); - setGlobalFilters((otherFilters) => ({ - ...otherFilters, - collaboratorsCount: [Number(e.target.value), otherFilters.collaboratorsCount?.[1]], - })); - }} - onKeyDown={inputStopPropagation} - onClick={(e) => e.stopPropagation()} - /> - - - setGlobalFilters((otherFilters) => ({ - ...otherFilters, - collaboratorsCount: [0, Number(e.target.value)], - })) - } - onKeyDown={inputStopPropagation} - onClick={(e) => e.stopPropagation()} - /> -
- )} - + + ); + }, + }, + Watchers: { + key: 'watchersCount', + name: 'Watchers Count', + + renderHeaderCell: (p) => { + return ( + + ); + }, + }, + 'Open Issues': { + key: 'openIssuesCount', + name: 'Issues Count', + + renderHeaderCell: (p) => { + return ( + + ); + }, + }, + 'Closed Issues': { + key: 'closedIssuesCount', + name: 'Closed Issues Count', + + renderHeaderCell: (p) => { + return ( + + ); + }, + }, + 'Open PRs': { + key: 'openPullRequestsCount', + name: 'Open PRs Count', + + renderHeaderCell: (p) => { + return ( + + ); + }, + }, + 'Merged PRs': { + key: 'mergedPullRequestsCount', + name: 'Merged PRs Count', + + renderHeaderCell: (p) => { + return ( + + ); + }, + }, + Forks: { + key: 'forksCount', + name: 'Forks Count', + + renderHeaderCell: (p) => { + return ( + ); }, }, - // Watchers: "watchersCount", - // "Open Issues": "openIssuesCount", - // "Closed Issues": "closedIssuesCount", - // "Open PRs": "openPullRequestsCount", - // "Merged PRs": "mergedPullRequestsCount", - // Forks: "forksCount", } as const; const dataGridColumns = Object.entries(labels).map(([_, columnProps]) => columnProps); @@ -265,7 +405,7 @@ const RepositoriesTable = () => { /** * Uses globalFilters to filter the repos that are then passed to sortRepos * - * NOTE: We use some hacks like addings 'all' to the licenseName filter to + * NOTE: We use some hacks like adding 'all' to the licenseName filter to * make it easier to filter the repos. */ const filterRepos = useCallback( @@ -280,6 +420,30 @@ const RepositoriesTable = () => { (globalFilters.collaboratorsCount ? (globalFilters.collaboratorsCount?.[0] ?? 0) <= repo.collaboratorsCount && repo.collaboratorsCount <= (globalFilters.collaboratorsCount[1] ?? Infinity) + : true) && + (globalFilters.watchersCount + ? (globalFilters.watchersCount?.[0] ?? 0) <= repo.watchersCount && + repo.watchersCount <= (globalFilters.watchersCount[1] ?? Infinity) + : true) && + (globalFilters.openIssuesCount + ? (globalFilters.openIssuesCount?.[0] ?? 0) <= repo.openIssuesCount && + repo.openIssuesCount <= (globalFilters.openIssuesCount[1] ?? Infinity) + : true) && + (globalFilters.closedIssuesCount + ? (globalFilters.closedIssuesCount?.[0] ?? 0) <= repo.closedIssuesCount && + repo.closedIssuesCount <= (globalFilters.closedIssuesCount[1] ?? Infinity) + : true) && + (globalFilters.openPullRequestsCount + ? (globalFilters.openPullRequestsCount?.[0] ?? 0) <= repo.openPullRequestsCount && + repo.openPullRequestsCount <= (globalFilters.openPullRequestsCount[1] ?? Infinity) + : true) && + (globalFilters.mergedPullRequestsCount + ? (globalFilters.mergedPullRequestsCount?.[0] ?? 0) <= repo.mergedPullRequestsCount && + repo.mergedPullRequestsCount <= (globalFilters.mergedPullRequestsCount[1] ?? Infinity) + : true) && + (globalFilters.forksCount + ? (globalFilters.forksCount?.[0] ?? 0) <= repo.forksCount && + repo.forksCount <= (globalFilters.forksCount[1] ?? Infinity) : true) ); }); @@ -300,7 +464,7 @@ const RepositoriesTable = () => {
-
+
Date: Tue, 23 Jan 2024 15:09:05 -0500 Subject: [PATCH 24/78] feat: various clean up and adding styles to filters --- .../src/components/RepositoriesTable.tsx | 303 +++++++++++------- 1 file changed, 181 insertions(+), 122 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 80bdab6..7ee5109 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,11 +1,8 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable primer-react/no-system-props */ -/* eslint-disable react/jsx-no-comment-textnodes */ /* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable react/no-unescaped-entities */ -import { InfoIcon } from '@primer/octicons-react'; -import { Button, Tooltip } from '@primer/react'; -import { Flex, Text } from '@tremor/react'; +import { InfoIcon, TriangleDownIcon, TriangleUpIcon, XIcon } from '@primer/octicons-react'; +import { ActionList, Box, Button, Checkbox, FormControl, TextInput, Tooltip } from '@primer/react'; +import { Text } from '@tremor/react'; import DataGrid, { Column, type RenderHeaderCellProps, type SortColumn } from 'react-data-grid'; import { Popover } from 'react-tiny-popover'; @@ -18,15 +15,9 @@ function inputStopPropagation(event: React.KeyboardEvent) { event.stopPropagation(); } -function selectStopPropagation(event: React.KeyboardEvent) { - if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) { - event.stopPropagation(); - } -} - type Filter = { repositoryName?: string; - licenseName?: string[]; + licenseName?: Record; collaboratorsCount?: Array; watchersCount?: Array; openIssuesCount?: Array; @@ -36,6 +27,7 @@ type Filter = { forksCount?: Array; }; +// Renderer for the min/max filter inputs const MinMaxRenderer: FC<{ headerCellProps: RenderHeaderCellProps; filters: Filter; @@ -46,51 +38,43 @@ const MinMaxRenderer: FC<{ {...headerCellProps}> {({ ...rest }) => (
- - { - updateFilters((globalFilters) => ({ - ...globalFilters, - [filterName]: [Number(e.target.value), globalFilters[filterName]?.[1]], - })); - }} - onKeyDown={inputStopPropagation} - onClick={(e) => e.stopPropagation()} - /> - - - updateFilters({ - ...filters, - [filterName]: [0, Number(e.target.value)], - }) - } - onKeyDown={inputStopPropagation} - onClick={(e) => e.stopPropagation()} - /> + + Min + { + updateFilters((globalFilters) => ({ + ...globalFilters, + [filterName]: [Number(e.target.value), (globalFilters[filterName] as Array)?.[1]], + })); + }} + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} /> + + + Max + + updateFilters({ + ...filters, + [filterName]: [0, Number(e.target.value)], + }) + } + onKeyDown={inputStopPropagation} + onClick={(e) => e.stopPropagation()} /> +
)} ); }; -/** - * Wrapper for rendering column header cell - * @param { - * tabIndex: number; - * column: Column; - * children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; - * } props - * @returns - */ +// Wrapper for rendering column header cell const FilterRenderer = ({ tabIndex, column, @@ -100,14 +84,18 @@ const FilterRenderer = ({ children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; }) => { const filters = useContext(FilterContext)!; - const clickMeButtonRef = useRef(); + const clickMeButtonRef = useRef(null); const [isPopoverOpen, setIsPopoverOpen] = useState(false); return (
{column.name}
- {sortDirection === 'ASC' ? 'UP' : sortDirection === 'DESC' ? 'DOWN' : null} + {sortDirection === 'DESC' ? ( + + ) : sortDirection === 'ASC' ? ( + + ) : } ({ onClickOutside={() => setIsPopoverOpen(false)} ref={clickMeButtonRef} // if you'd like a ref to your popover's child, you can grab one here content={() => ( -
e.stopPropagation()}> -
-

Filter {column.name}

- {filterFunction({ tabIndex, filters })} +
e.stopPropagation()}> +
+ + Filter by {column.name} + {filterFunction({ tabIndex, filters })} +
)} > -
-
+
+
); }; @@ -183,11 +172,15 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { } }; +// Default set of filters +const defaultFilters: Filter = { + licenseName: { + all: true, + } +} + const RepositoriesTable = () => { - const [globalFilters, setGlobalFilters] = useState({ - repositoryName: undefined, - licenseName: [], - } as Filter); + const [globalFilters, setGlobalFilters] = useState(defaultFilters); // This needs a type, technically it's a Column but needs to be typed const labels: Record> = { @@ -199,7 +192,7 @@ const RepositoriesTable = () => { return ( {...p}> {({ filters, ...rest }) => ( - @@ -221,42 +214,91 @@ const RepositoriesTable = () => { name: 'License', renderHeaderCell: (p) => { + // This is fine because we know it's going to be rendered as a component + // eslint-disable-next-line react-hooks/rules-of-hooks + const [filteredOptions, setFilteredOptions] = useState(''); + return ( {...p}> - {({ filters, ...rest }) => ( - + return ( + <> + { + setGlobalFilters((otherFilters) => ({ + ...otherFilters, + licenseName: { + ...otherFilters.licenseName, + [d.value]: !otherFilters.licenseName?.[d.value], + }, + })); + }} + > + + + + {d.value} + + + ); + })} + + )} ); @@ -264,7 +306,7 @@ const RepositoriesTable = () => { }, Collaborators: { key: 'collaboratorsCount', - name: 'Collaborator Count', + name: 'Collaborators', renderHeaderCell: (p) => { return ( { }, Watchers: { key: 'watchersCount', - name: 'Watchers Count', + name: 'Watchers', renderHeaderCell: (p) => { return ( @@ -293,7 +335,7 @@ const RepositoriesTable = () => { }, 'Open Issues': { key: 'openIssuesCount', - name: 'Issues Count', + name: 'Open Issues', renderHeaderCell: (p) => { return ( @@ -308,7 +350,7 @@ const RepositoriesTable = () => { }, 'Closed Issues': { key: 'closedIssuesCount', - name: 'Closed Issues Count', + name: 'Closed Issues', renderHeaderCell: (p) => { return ( @@ -323,7 +365,7 @@ const RepositoriesTable = () => { }, 'Open PRs': { key: 'openPullRequestsCount', - name: 'Open PRs Count', + name: 'Open PRs', renderHeaderCell: (p) => { return ( @@ -338,7 +380,7 @@ const RepositoriesTable = () => { }, 'Merged PRs': { key: 'mergedPullRequestsCount', - name: 'Merged PRs Count', + name: 'Merged PRs', renderHeaderCell: (p) => { return ( @@ -353,7 +395,7 @@ const RepositoriesTable = () => { }, Forks: { key: 'forksCount', - name: 'Forks Count', + name: 'Total Forks', renderHeaderCell: (p) => { return ( @@ -368,6 +410,7 @@ const RepositoriesTable = () => { }, } as const; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const dataGridColumns = Object.entries(labels).map(([_, columnProps]) => columnProps); const subTitle = () => { @@ -375,11 +418,13 @@ const RepositoriesTable = () => { }; // This selects a field to populate a dropdown with - const dropdownOptions = (field: keyof Repo) => - Array.from(new Set(repos.map((r) => r[field]))).map((d) => ({ - label: d, - value: d, - })); + const dropdownOptions = (field: keyof Repo, filter = '') => + Array.from(new Set(repos.map((r) => r[field]))) + .map((d) => ({ + label: d, + value: d, + })) + .filter((d) => (d.value as string).toLowerCase().includes(filter.toLowerCase())); const [sortColumns, setSortColumns] = useState([]); @@ -407,16 +452,15 @@ const RepositoriesTable = () => { * * NOTE: We use some hacks like adding 'all' to the licenseName filter to * make it easier to filter the repos. + * + * This is kind of a mess, but it works */ const filterRepos = useCallback( (inputRepos: Repo[]) => { const result = inputRepos.filter((repo) => { return ( (globalFilters.repositoryName ? repo.repositoryName.includes(globalFilters.repositoryName) : true) && - ((globalFilters.licenseName?.length ?? 0 > 0 - ? globalFilters.licenseName?.includes(repo.licenseName) - : true) || - globalFilters.licenseName?.includes('all')) && + ((globalFilters.licenseName?.[repo.licenseName] ?? false) || (globalFilters.licenseName?.['all'] ?? false)) && (globalFilters.collaboratorsCount ? (globalFilters.collaboratorsCount?.[0] ?? 0) <= repo.collaboratorsCount && repo.collaboratorsCount <= (globalFilters.collaboratorsCount[1] ?? Infinity) @@ -455,13 +499,28 @@ const RepositoriesTable = () => { return (
-
- - - - - {subTitle()} - +
+
+
+ + + + {subTitle()} +
+
+ + + +
+
@@ -480,7 +539,7 @@ const RepositoriesTable = () => { />
-
+
); }; From 9b74f0f76b59cb3f196be1510119b4d4360907ce Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 23 Jan 2024 15:34:53 -0500 Subject: [PATCH 25/78] feat: make table fill window --- .../src/components/OrganizationSheet.tsx | 8 +++--- .../src/components/RepositoriesTable.tsx | 26 +++++++++---------- who-metrics-ui/src/pages/index.tsx | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/who-metrics-ui/src/components/OrganizationSheet.tsx b/who-metrics-ui/src/components/OrganizationSheet.tsx index 1a1ade6..0981cf5 100644 --- a/who-metrics-ui/src/components/OrganizationSheet.tsx +++ b/who-metrics-ui/src/components/OrganizationSheet.tsx @@ -26,7 +26,7 @@ export const OrganizationSheet = () => { } return ( -
+
{ This project includes metrics about the Open Source repositories for the {data.orgInfo.name}. - + Repositories - - + + diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 7ee5109..f23334b 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { InfoIcon, TriangleDownIcon, TriangleUpIcon, XIcon } from '@primer/octicons-react'; import { ActionList, Box, Button, Checkbox, FormControl, TextInput, Tooltip } from '@primer/react'; import { Text } from '@tremor/react'; @@ -35,7 +33,7 @@ const MinMaxRenderer: FC<{ filterName: keyof Filter; }> = ({ headerCellProps, filters, updateFilters, filterName }) => { return ( - {...headerCellProps}> + {...headerCellProps}> {({ ...rest }) => (
@@ -70,12 +68,12 @@ const MinMaxRenderer: FC<{
)} - + ); }; // Wrapper for rendering column header cell -const FilterRenderer = ({ +const HeaderCellRenderer = ({ tabIndex, column, children: filterFunction, @@ -103,6 +101,8 @@ const FilterRenderer = ({ onClickOutside={() => setIsPopoverOpen(false)} ref={clickMeButtonRef} // if you'd like a ref to your popover's child, you can grab one here content={() => ( + // The click handler here is used to stop the header from being sorted + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
e.stopPropagation()}>
@@ -190,7 +190,7 @@ const RepositoriesTable = () => { renderHeaderCell: (p) => { return ( - {...p}> + {...p}> {({ filters, ...rest }) => ( { onClick={(e) => e.stopPropagation()} /> )} - + ); }, }, @@ -219,7 +219,7 @@ const RepositoriesTable = () => { const [filteredOptions, setFilteredOptions] = useState(''); return ( - {...p}> + {...p}> {({ ...rest }) => ( { )} - + ); }, }, @@ -498,8 +498,8 @@ const RepositoriesTable = () => { ); return ( -
-
+
+
@@ -523,9 +523,9 @@ const RepositoriesTable = () => {
-
+ { /* This is a weird hack to make the table fill the page */} +
repo.repoName} diff --git a/who-metrics-ui/src/pages/index.tsx b/who-metrics-ui/src/pages/index.tsx index 2374d8e..e184397 100644 --- a/who-metrics-ui/src/pages/index.tsx +++ b/who-metrics-ui/src/pages/index.tsx @@ -3,7 +3,7 @@ import { OrganizationSheet } from "../components/OrganizationSheet"; export default function PlaygroundPage() { return ( -
+
); From 7b882aebc9ac4a3c1abbac9a92707f8102db1842 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 23 Jan 2024 15:38:06 -0500 Subject: [PATCH 26/78] feat: add prettier to dev deps --- who-metrics-ui/package-lock.json | 30 +++++++++++++++++++++++------- who-metrics-ui/package.json | 4 ++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/who-metrics-ui/package-lock.json b/who-metrics-ui/package-lock.json index f137533..d34b0f0 100644 --- a/who-metrics-ui/package-lock.json +++ b/who-metrics-ui/package-lock.json @@ -28,7 +28,6 @@ "next-auth": "^4.19.2", "next-themes": "^0.2.1", "postcss": "^8.4.31", - "prettier": "^2.8.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-data-grid": "^7.0.0-beta.41", @@ -50,7 +49,8 @@ "eslint-plugin-github": "^4.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-primer-react": "^3.0.0", - "eslint-plugin-react": "^7.32.2" + "eslint-plugin-react": "^7.32.2", + "prettier": "^3.2.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2933,6 +2933,21 @@ "eslint": "^8.0.1" } }, + "node_modules/eslint-plugin-github/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/eslint-plugin-i18n-text": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", @@ -4948,14 +4963,15 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" diff --git a/who-metrics-ui/package.json b/who-metrics-ui/package.json index 14a08be..acf311f 100644 --- a/who-metrics-ui/package.json +++ b/who-metrics-ui/package.json @@ -29,7 +29,6 @@ "next-auth": "^4.19.2", "next-themes": "^0.2.1", "postcss": "^8.4.31", - "prettier": "^2.8.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-data-grid": "^7.0.0-beta.41", @@ -52,6 +51,7 @@ "eslint-plugin-github": "^4.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-primer-react": "^3.0.0", - "eslint-plugin-react": "^7.32.2" + "eslint-plugin-react": "^7.32.2", + "prettier": "^3.2.4" } } From b847b55806c1bfe9af670cc85e3b434636960271 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 23 Jan 2024 15:41:33 -0500 Subject: [PATCH 27/78] chore: update line width from 120 to 80 --- who-metrics-ui/.prettierrc | 2 +- .../src/components/OrganizationSheet.tsx | 32 ++-- .../src/components/RepositoriesTable.tsx | 169 +++++++++++++----- 3 files changed, 142 insertions(+), 61 deletions(-) diff --git a/who-metrics-ui/.prettierrc b/who-metrics-ui/.prettierrc index 6d99a58..e6509bf 100644 --- a/who-metrics-ui/.prettierrc +++ b/who-metrics-ui/.prettierrc @@ -1,6 +1,6 @@ { "singleQuote": true, "trailingComma": "all", - "printWidth": 120, + "printWidth": 80, "tabWidth": 2 } diff --git a/who-metrics-ui/src/components/OrganizationSheet.tsx b/who-metrics-ui/src/components/OrganizationSheet.tsx index 0981cf5..7c282cf 100644 --- a/who-metrics-ui/src/components/OrganizationSheet.tsx +++ b/who-metrics-ui/src/components/OrganizationSheet.tsx @@ -1,27 +1,31 @@ -"use client"; +'use client'; import { - Tab, TabGroup, TabList, TabPanel, - TabPanels, Text, Title -} from "@tremor/react"; + Tab, + TabGroup, + TabList, + TabPanel, + TabPanels, + Text, + Title +} from '@tremor/react'; -import logo from "@/images/who-logo-wide.svg"; -import { Box, useTheme as primerUseTheme } from "@primer/react"; -import Image from "next/image"; - -import { useTheme } from "next-themes"; -import data from "../data/data.json"; -import RepositoriesTable from "./RepositoriesTable"; +import logo from '@/images/who-logo-wide.svg'; +import { Box, useTheme as primerUseTheme } from '@primer/react'; +import Image from 'next/image'; +import { useTheme } from 'next-themes'; +import data from '../data/data.json'; +import RepositoriesTable from './RepositoriesTable'; export const OrganizationSheet = () => { const { theme, systemTheme } = useTheme(); const { setColorMode } = primerUseTheme(); - if (theme === "light" || theme === "dark" || theme === "auto") { + if (theme === 'light' || theme === 'dark' || theme === 'auto') { setColorMode(theme); } - if (theme === "system" && systemTheme) { + if (theme === 'system' && systemTheme) { setColorMode(systemTheme); } @@ -29,7 +33,7 @@ export const OrganizationSheet = () => {
; filters: Filter; - updateFilters: ((filters: Filter) => void) & ((filters: (filters: Filter) => Filter) => void); + updateFilters: ((filters: Filter) => void) & + ((filters: (filters: Filter) => Filter) => void); filterName: keyof Filter; }> = ({ headerCellProps, filters, updateFilters, filterName }) => { return ( @@ -37,23 +62,35 @@ const MinMaxRenderer: FC<{ {({ ...rest }) => (
- Min - + Min + + { updateFilters((globalFilters) => ({ ...globalFilters, - [filterName]: [Number(e.target.value), (globalFilters[filterName] as Array)?.[1]], + [filterName]: [ + Number(e.target.value), + ( + globalFilters[filterName] as Array + )?.[1], + ], })); }} onKeyDown={inputStopPropagation} - onClick={(e) => e.stopPropagation()} /> + onClick={(e) => e.stopPropagation()} + /> - Max - + Max + + e.stopPropagation()} /> + onClick={(e) => e.stopPropagation()} + />
)} @@ -93,7 +131,9 @@ const HeaderCellRenderer = ({ ) : sortDirection === 'ASC' ? ( - ) : } + ) : ( + + )} ({ content={() => ( // The click handler here is used to stop the header from being sorted // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
e.stopPropagation()}> +
e.stopPropagation()} + >
Filter by {column.name} @@ -124,8 +167,8 @@ const HeaderCellRenderer = ({ Filters -
-
+
+
); }; @@ -165,7 +208,9 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { case 'repoName': case 'repositoryName': return (a, b) => { - return a[sortColumn].toLowerCase().localeCompare(b[sortColumn].toLowerCase()); + return a[sortColumn] + .toLowerCase() + .localeCompare(b[sortColumn].toLowerCase()); }; default: throw new Error(`unsupported sortColumn: "${sortColumn}"`); @@ -176,8 +221,8 @@ const getComparator = (sortColumn: keyof Repo): Comparator => { const defaultFilters: Filter = { licenseName: { all: true, - } -} + }, +}; const RepositoriesTable = () => { const [globalFilters, setGlobalFilters] = useState(defaultFilters); @@ -243,12 +288,18 @@ const RepositoriesTable = () => { onClick={() => { setGlobalFilters((otherFilters) => ({ ...otherFilters, - licenseName: { ...otherFilters.licenseName, all: !otherFilters.licenseName?.['all'] }, + licenseName: { + ...otherFilters.licenseName, + all: !otherFilters.licenseName?.['all'], + }, })); }} > - + All @@ -262,13 +313,19 @@ const RepositoriesTable = () => { ...otherFilters, licenseName: { ...otherFilters.licenseName, - [d.value]: !otherFilters.licenseName?.[d.value], + [d.value]: + !otherFilters.licenseName?.[d.value], }, })); }} > - + No License @@ -290,7 +347,12 @@ const RepositoriesTable = () => { }} > - + {d.value} @@ -410,8 +472,10 @@ const RepositoriesTable = () => { }, } as const; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const dataGridColumns = Object.entries(labels).map(([_, columnProps]) => columnProps); + const dataGridColumns = Object.entries(labels).map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ([_, columnProps]) => columnProps, + ); const subTitle = () => { return `${repos.length} total repositories`; @@ -424,7 +488,9 @@ const RepositoriesTable = () => { label: d, value: d, })) - .filter((d) => (d.value as string).toLowerCase().includes(filter.toLowerCase())); + .filter((d) => + (d.value as string).toLowerCase().includes(filter.toLowerCase()), + ); const [sortColumns, setSortColumns] = useState([]); @@ -452,38 +518,51 @@ const RepositoriesTable = () => { * * NOTE: We use some hacks like adding 'all' to the licenseName filter to * make it easier to filter the repos. - * + * * This is kind of a mess, but it works */ const filterRepos = useCallback( (inputRepos: Repo[]) => { const result = inputRepos.filter((repo) => { return ( - (globalFilters.repositoryName ? repo.repositoryName.includes(globalFilters.repositoryName) : true) && - ((globalFilters.licenseName?.[repo.licenseName] ?? false) || (globalFilters.licenseName?.['all'] ?? false)) && + (globalFilters.repositoryName + ? repo.repositoryName.includes(globalFilters.repositoryName) + : true) && + ((globalFilters.licenseName?.[repo.licenseName] ?? false) || + (globalFilters.licenseName?.['all'] ?? false)) && (globalFilters.collaboratorsCount - ? (globalFilters.collaboratorsCount?.[0] ?? 0) <= repo.collaboratorsCount && - repo.collaboratorsCount <= (globalFilters.collaboratorsCount[1] ?? Infinity) + ? (globalFilters.collaboratorsCount?.[0] ?? 0) <= + repo.collaboratorsCount && + repo.collaboratorsCount <= + (globalFilters.collaboratorsCount[1] ?? Infinity) : true) && (globalFilters.watchersCount ? (globalFilters.watchersCount?.[0] ?? 0) <= repo.watchersCount && repo.watchersCount <= (globalFilters.watchersCount[1] ?? Infinity) : true) && (globalFilters.openIssuesCount - ? (globalFilters.openIssuesCount?.[0] ?? 0) <= repo.openIssuesCount && - repo.openIssuesCount <= (globalFilters.openIssuesCount[1] ?? Infinity) + ? (globalFilters.openIssuesCount?.[0] ?? 0) <= + repo.openIssuesCount && + repo.openIssuesCount <= + (globalFilters.openIssuesCount[1] ?? Infinity) : true) && (globalFilters.closedIssuesCount - ? (globalFilters.closedIssuesCount?.[0] ?? 0) <= repo.closedIssuesCount && - repo.closedIssuesCount <= (globalFilters.closedIssuesCount[1] ?? Infinity) + ? (globalFilters.closedIssuesCount?.[0] ?? 0) <= + repo.closedIssuesCount && + repo.closedIssuesCount <= + (globalFilters.closedIssuesCount[1] ?? Infinity) : true) && (globalFilters.openPullRequestsCount - ? (globalFilters.openPullRequestsCount?.[0] ?? 0) <= repo.openPullRequestsCount && - repo.openPullRequestsCount <= (globalFilters.openPullRequestsCount[1] ?? Infinity) + ? (globalFilters.openPullRequestsCount?.[0] ?? 0) <= + repo.openPullRequestsCount && + repo.openPullRequestsCount <= + (globalFilters.openPullRequestsCount[1] ?? Infinity) : true) && (globalFilters.mergedPullRequestsCount - ? (globalFilters.mergedPullRequestsCount?.[0] ?? 0) <= repo.mergedPullRequestsCount && - repo.mergedPullRequestsCount <= (globalFilters.mergedPullRequestsCount[1] ?? Infinity) + ? (globalFilters.mergedPullRequestsCount?.[0] ?? 0) <= + repo.mergedPullRequestsCount && + repo.mergedPullRequestsCount <= + (globalFilters.mergedPullRequestsCount[1] ?? Infinity) : true) && (globalFilters.forksCount ? (globalFilters.forksCount?.[0] ?? 0) <= repo.forksCount && @@ -498,8 +577,8 @@ const RepositoriesTable = () => { ); return ( -
-
+
+
@@ -517,13 +596,11 @@ const RepositoriesTable = () => { > Clear All Filters - -
- { /* This is a weird hack to make the table fill the page */} + {/* This is a weird hack to make the table fill the page */}
{ />
-
+
); }; From 5b109cb8f3614e0a3599a245b0886373c7bd5e95 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 23 Jan 2024 15:48:45 -0500 Subject: [PATCH 28/78] Update who-metrics-ui/.eslintrc.js Co-authored-by: Ian Candy --- who-metrics-ui/.eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/who-metrics-ui/.eslintrc.js b/who-metrics-ui/.eslintrc.js index 926ee62..95e1d36 100644 --- a/who-metrics-ui/.eslintrc.js +++ b/who-metrics-ui/.eslintrc.js @@ -63,7 +63,7 @@ const baseConfig = { }, ignorePatterns: ["*__generated__*"], rules: { - "prettier/prettier": 0, // Sorry but prettier/prettier is quite problematic + "prettier/prettier": 0, // We use prettier for formatting instead of ESLint "import/no-unresolved": [ "error", { From da34e53e84d3ef189e5280d006a28fb84e4b86e8 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 24 Jan 2024 14:50:44 -0500 Subject: [PATCH 29/78] fix: dark theme for filters and add searchable filter for repo name --- .../src/components/RepositoriesTable.tsx | 334 ++++++++++-------- who-metrics-ui/tailwind.config.js | 17 +- 2 files changed, 196 insertions(+), 155 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index ae9b8ed..8b8328b 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -38,7 +38,7 @@ function inputStopPropagation(event: React.KeyboardEvent) { } type Filter = { - repositoryName?: string; + repositoryName?: Record; licenseName?: Record; collaboratorsCount?: Array; watchersCount?: Array; @@ -49,6 +49,17 @@ type Filter = { forksCount?: Array; }; +// This selects a field to populate a dropdown with +const dropdownOptions = (field: keyof Repo, filter = '') => + Array.from(new Set(repos.map((r) => r[field]))) + .map((d) => ({ + label: d, + value: d, + })) + .filter((d) => + (d.value as string).toLowerCase().includes(filter.toLowerCase()), + ); + // Renderer for the min/max filter inputs const MinMaxRenderer: FC<{ headerCellProps: RenderHeaderCellProps; @@ -60,7 +71,7 @@ const MinMaxRenderer: FC<{ return ( {...headerCellProps}> {({ ...rest }) => ( -
+ Min @@ -68,6 +79,7 @@ const MinMaxRenderer: FC<{ { @@ -92,6 +104,7 @@ const MinMaxRenderer: FC<{ @@ -104,7 +117,138 @@ const MinMaxRenderer: FC<{ onClick={(e) => e.stopPropagation()} /> -
+ + )} + + ); +}; + +// Renderer for the searchable select filter +const SearchableSelectRenderer: FC<{ + headerCellProps: RenderHeaderCellProps; + filters: Filter; + updateFilters: ((filters: Filter) => void) & + ((filters: (filters: Filter) => Filter) => void); + filterName: keyof Filter; +}> = ({ headerCellProps, filters, updateFilters, filterName }) => { + const [filteredOptions, setFilteredOptions] = useState(''); + const allSelectOptions = dropdownOptions(filterName, filteredOptions); + + return ( + {...headerCellProps}> + {({ ...rest }) => ( + + setFilteredOptions(e.target.value)} + trailingAction={ + { + setFilteredOptions(''); + }} + icon={XIcon} + aria-label="Clear input" + sx={{ color: 'fg.subtle' }} + /> + } + /> + + + { + updateFilters((otherFilters) => ({ + ...otherFilters, + [filterName]: { + ...otherFilters[filterName], + all: !( + otherFilters[filterName] as Record + )?.['all'], + }, + })); + }} + > + + )?.['all'] ?? + true + } + /> + + All + + {allSelectOptions.map((d) => { + if (d.value === '') { + return ( + <> + { + updateFilters((otherFilters) => ({ + ...otherFilters, + [filterName]: { + ...otherFilters[filterName], + [d.value]: !( + otherFilters[filterName] as Record< + string, + boolean + > + )?.[d.value], + }, + })); + }} + > + + )?.[ + d.value + ] ?? false + } + /> + + No License + + + ); + } + + return ( + <> + { + updateFilters((otherFilters) => ({ + ...otherFilters, + [filterName]: { + ...otherFilters[filterName], + [d.value]: !( + otherFilters[filterName] as Record + )?.[d.value], + }, + })); + }} + > + + )?.[ + d.value + ] ?? false + } + /> + + {d.value} + + + ); + })} + + + )} ); @@ -142,18 +286,20 @@ const HeaderCellRenderer = ({ ref={clickMeButtonRef} // if you'd like a ref to your popover's child, you can grab one here content={() => ( // The click handler here is used to stop the header from being sorted - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
e.stopPropagation()} + sx={{ + backgroundColor: 'Background', + border: '1px solid', + borderColor: 'border.default', + }} > -
- - Filter by {column.name} - {filterFunction({ tabIndex, filters })} - -
-
+ + Filter by {column.name} + {filterFunction({ tabIndex, filters })} + + )} > +
@@ -604,7 +637,7 @@ const RepositoriesTable = () => {
repo.repoName} defaultColumnOptions={{ sortable: true, From b425883573cc10a50f06a30540b555a76de85174 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 24 Jan 2024 16:33:07 -0500 Subject: [PATCH 31/78] fix: formatting --- who-metrics-ui/src/components/RepositoriesTable.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 4ef6490..55bafcf 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -229,7 +229,7 @@ const defaultFilters: Filter = { const generateCSV = (data: Repo[]): Blob => { const output = json2csv(data); return new Blob([output], { type: 'text/csv' }); -} +}; // Helper for downloading the csv // There are warnings for Opera Mini incompatibility, but only 0.07% of users use that browser @@ -245,8 +245,6 @@ const saveFile = async (contents: Blob) => { a.click(); }; - - const RepositoriesTable = () => { const [globalFilters, setGlobalFilters] = useState(defaultFilters); @@ -599,7 +597,7 @@ const RepositoriesTable = () => { [globalFilters], ); - const displayRows = filterRepos(sortRepos(repos)) + const displayRows = filterRepos(sortRepos(repos)); return (
@@ -611,7 +609,7 @@ const RepositoriesTable = () => { {subTitle()}
-
+
From 81ba97f319bb3ea74de2f88815cbaed2162fb630 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 29 Feb 2024 16:13:30 -0500 Subject: [PATCH 75/78] ;-) --- who-metrics-ui/src/components/RepositoriesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 032f198..bf2d558 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -96,7 +96,7 @@ const dropdownOptions = (field: keyof RepositoryResult, filter = ''): SelectOpti .filter((fieldName) => (fieldName.value as string).toLowerCase().includes(filter.toLowerCase()), ); -} +}; // Helper function to get the selected option value from a filter and field const getSelectedOption = ( From 9f949e80fdcfeee14b93eecbbf3277e1bfa85e93 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Fri, 1 Mar 2024 19:42:10 +0000 Subject: [PATCH 76/78] Feature: Extract TopicCell component - Extracts a TopicCell component to fix a linter error with using a hook in the wrong context. This also had the benefit of making the props easier to work with. - Use the Primer `Label` to render the topics to make it look pretty! --- .../src/components/RepositoriesTable.tsx | 32 ++------------- who-metrics-ui/src/components/TopicCell.tsx | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 who-metrics-ui/src/components/TopicCell.tsx diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index bf2d558..eaca36b 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -34,6 +34,7 @@ import { import { RepositoryResult } from '../../../ts-backend/src'; import Data from '../data/data.json'; +import TopicCell from './TopicCell'; const repos = Object.values(Data['repositories']); function inputStopPropagation(event: React.KeyboardEvent) { @@ -490,36 +491,9 @@ const RepositoriesTable = () => { ) }, renderCell: (props) => { - const [isHovering, setIsHovering] = useState(false) - // tabIndex === 0 is used as a proxy when the Cell is selected. See https://github.com/adazzle/react-data-grid/pull/3236 - const isOpen = props.row.topics.length > 0 && (isHovering || props.tabIndex === 0) - - return ( - { - return ( - e.stopPropagation()} - sx={{ - backgroundColor: 'Background', - border: '1px solid', - borderColor: 'border.default', - }} - > - {props.row.topics.sort().join(", ")} - - ) - }}> - setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - > - {props.row.topics.sort().join(', ')} - - - ) + const isSelected = props.tabIndex === 0 + return }, }, License: { diff --git a/who-metrics-ui/src/components/TopicCell.tsx b/who-metrics-ui/src/components/TopicCell.tsx new file mode 100644 index 0000000..2c22abd --- /dev/null +++ b/who-metrics-ui/src/components/TopicCell.tsx @@ -0,0 +1,40 @@ +import { useState } from "react" +import { Popover } from 'react-tiny-popover' +import { Box, Label} from '@primer/react' + +const TopicCell = ({topics, isSelected}: { + topics: string[] + isSelected: boolean +}) => { + const [isHovering, setIsHovering] = useState(false) + const isOpen = topics.length > 0 && (isHovering || isSelected) + + return ( + { + return ( + e.stopPropagation()} + sx={{ + backgroundColor: 'Background', + border: '1px solid', + borderColor: 'border.default', + }} + > + {topics.sort().map((topic) => )} + + ) + }}> + setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + className="space-x-1 m-1" + > + {topics.sort().map((topic) => )} + + + ) +} + +export default TopicCell \ No newline at end of file From d723b969321b7132d7a0bd79627023cab22d7ab2 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Fri, 1 Mar 2024 19:51:41 +0000 Subject: [PATCH 77/78] Feature: Move type to shared directory The app did not build correctly when the shared type was in the ts-backend folder due to needing to process the entire contents of that file, so instead we move the type to a shared location. In the future we may define in one spot and then move automatically as a prepare step, but this should be okay for now. --- ts-backend/src/fetchers/repository.ts | 3 +- ts-backend/src/index.ts | 38 +------------------ types/index.ts | 36 ++++++++++++++++++ .../src/components/RepositoriesTable.tsx | 2 +- 4 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 types/index.ts diff --git a/ts-backend/src/fetchers/repository.ts b/ts-backend/src/fetchers/repository.ts index 53e5575..5934637 100644 --- a/ts-backend/src/fetchers/repository.ts +++ b/ts-backend/src/fetchers/repository.ts @@ -1,7 +1,8 @@ // Fetchers for repository data and metrics import { Organization, Repository } from "@octokit/graphql-schema"; -import { Fetcher, RepositoryResult } from ".."; +import { Fetcher } from ".."; +import { RepositoryResult } from '../../../types' export const addRepositoriesToResult: Fetcher = async ( result, diff --git a/ts-backend/src/index.ts b/ts-backend/src/index.ts index df15995..0c2a496 100644 --- a/ts-backend/src/index.ts +++ b/ts-backend/src/index.ts @@ -12,6 +12,7 @@ import { addRepositoriesToResult, } from "./fetchers"; import { checkRateLimit, CustomOctokit, personalOctokit } from "./lib/octokit"; +import { RepositoryResult } from '../../types' export interface Result { meta: { @@ -31,43 +32,6 @@ export interface Result { repositories: Record; } -export interface RepositoryResult { - // Repo metadata - repositoryName: string; - repoNameWithOwner: string; - licenseName: string; - topics: string[]; - - // Counts of various things - projectsCount: number; - projectsV2Count: number; - discussionsCount: number; - forksCount: number; - totalIssuesCount: number; - openIssuesCount: number; - closedIssuesCount: number; - totalPullRequestsCount: number; - openPullRequestsCount: number; - closedPullRequestsCount: number; - mergedPullRequestsCount: number; - watchersCount: number; - starsCount: number; - collaboratorsCount: number; - - // Flags - discussionsEnabled: boolean; - projectsEnabled: boolean; - issuesEnabled: boolean; - - // Calculated metrics - openIssuesAverageAge: number; - openIssuesMedianAge: number; - closedIssuesAverageAge: number; - closedIssuesMedianAge: number; - issuesResponseAverageAge: number; - issuesResponseMedianAge: number; -} - export type Fetcher = ( result: Result, octokit: CustomOctokit, diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..146b960 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,36 @@ +export interface RepositoryResult { + // Repo metadata + repositoryName: string; + repoNameWithOwner: string; + licenseName: string; + topics: string[]; + + // Counts of various things + projectsCount: number; + projectsV2Count: number; + discussionsCount: number; + forksCount: number; + totalIssuesCount: number; + openIssuesCount: number; + closedIssuesCount: number; + totalPullRequestsCount: number; + openPullRequestsCount: number; + closedPullRequestsCount: number; + mergedPullRequestsCount: number; + watchersCount: number; + starsCount: number; + collaboratorsCount: number; + + // Flags + discussionsEnabled: boolean; + projectsEnabled: boolean; + issuesEnabled: boolean; + + // Calculated metrics + openIssuesAverageAge: number; + openIssuesMedianAge: number; + closedIssuesAverageAge: number; + closedIssuesMedianAge: number; + issuesResponseAverageAge: number; + issuesResponseMedianAge: number; +} \ No newline at end of file diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index eaca36b..483e133 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -32,7 +32,7 @@ import { useState } from 'react'; -import { RepositoryResult } from '../../../ts-backend/src'; +import { RepositoryResult } from '../../../types'; import Data from '../data/data.json'; import TopicCell from './TopicCell'; From 17298dcea0f4913cd7a342d0991b940bd6e6eb54 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Fri, 1 Mar 2024 14:55:59 -0500 Subject: [PATCH 78/78] Fix: Use implicit checks for truthiness Co-authored-by: Andrew Henry --- who-metrics-ui/src/components/RepositoriesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/who-metrics-ui/src/components/RepositoriesTable.tsx b/who-metrics-ui/src/components/RepositoriesTable.tsx index 483e133..2234d8e 100644 --- a/who-metrics-ui/src/components/RepositoriesTable.tsx +++ b/who-metrics-ui/src/components/RepositoriesTable.tsx @@ -419,8 +419,8 @@ const getComparator = (sortColumn: keyof RepositoryResult): Comparator => { return (a, b) => { const first = a[sortColumn].sort()[0] const second = b[sortColumn].sort()[0] - if (second === undefined) return -1; - if (first === undefined) return 1; + if (!second) return -1; + if (!first) return 1; return first.toLowerCase().localeCompare(second.toLowerCase()) };