From fc72b08aa32c98256557fef27dcfed270b930def Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 9 Nov 2023 18:47:17 +0000 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 6/7] 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 7/7] 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: {