From a68c0d31fd63bc39d09c7b26a8cf9e3642944e74 Mon Sep 17 00:00:00 2001 From: Zhihao Cui Date: Fri, 12 Jan 2024 14:38:44 +0000 Subject: [PATCH] Fix site roadmap page not populating WIP items (#2905) Co-authored-by: Alina Visan --- site/src/components/roadmap/Roadmap.tsx | 255 ++++++++++--------- site/src/components/roadmap/style.module.css | 5 - site/src/pages/api/roadmap.ts | 55 +++- 3 files changed, 177 insertions(+), 138 deletions(-) diff --git a/site/src/components/roadmap/Roadmap.tsx b/site/src/components/roadmap/Roadmap.tsx index 0912c0f6bbb..fa975efe661 100644 --- a/site/src/components/roadmap/Roadmap.tsx +++ b/site/src/components/roadmap/Roadmap.tsx @@ -1,60 +1,48 @@ -import { Key, ReactNode, useEffect, useState } from "react"; import { - GridLayout, - Input, Banner, BannerContent, + GridLayout, + H3, + FormField, + FormFieldLabel, + Input, InteractableCard, InteractableCardProps, - H3, Spinner, + Text, } from "@salt-ds/core"; - -import styles from "./style.module.css"; import { FilterIcon, ProgressInprogressIcon, ProgressTodoIcon, } from "@salt-ds/icons"; - -import { formatDate } from "src/utils/formatDate"; +import { ReactNode, useEffect, useState } from "react"; import { Heading4 } from "../mdx/h4"; -type RoadmapProps = { title: string; children: ReactNode; endpoint: string }; +import styles from "./style.module.css"; -interface RoadmapData { - content: any; - fieldValues: any; - id: string; - startDate: Date; - targetDate: Date; - issueUrl: string; - text: string; +interface RoadmapProps { + title: string; + children: ReactNode; + endpoint: string; } -interface CardViewProps { - data: RoadmapData[]; - searchQuery: string; +interface IterationData { + title: string; // e.g. Q1 + startDate: string; // e.g. "2024-01-01" + duration: number; // e.g. 91 } -function sortRoadmapDataByDate(roadmapData: RoadmapData[]): RoadmapData[] { - const sortedData = [...roadmapData]; - sortedData.sort((a, b) => { - const startDateA = new Date(a.targetDate); - const startDateB = new Date(b.targetDate); - - if (isNaN(startDateA.getTime())) { - return 1; // Item A doesn't have a valid date, so it should be placed below item B - } else if (isNaN(startDateB.getTime())) { - return -1; // Item B doesn't have a valid date, so it should be placed below item A - } else { - return startDateA.getTime() - startDateB.getTime(); - } - }); - return sortedData; +interface RoadmapData { + id: string; + startSprint: IterationData | null; + endSprint: IterationData | null; + issueUrl: string; + title: string; + quarter: IterationData | null; } -function RoadmapCard(props: InteractableCardProps, item: ItemProps) { +function RoadmapCard(props: InteractableCardProps) { return ( <> @@ -62,33 +50,27 @@ function RoadmapCard(props: InteractableCardProps, item: ItemProps) { ); } -export const Roadmap = ({ title, children, endpoint }: RoadmapProps) => { - const [roadmapData, setRoadmapData] = useState([]); - const sortedRoadmapData = sortRoadmapDataByDate(roadmapData); +export const Roadmap = ({ endpoint }: RoadmapProps) => { + const [roadmapData, setRoadmapData] = useState([]); const [searchQuery, setSearchQuery] = useState(""); + const filteredRoadmapData = roadmapData.filter( + (r) => + (r.quarter !== null || r.startSprint !== null || r.endSprint !== null) && + r.title.match(new RegExp(searchQuery, "i")) + ); + useEffect(() => { + const abortController = new AbortController(); + const fetchData = async () => { try { - const response = await fetch(`${endpoint}`); - const responseData = await response.json(); - - const items = - responseData?.data?.organization?.repository?.projectV2?.items?.nodes; - - //creates an array of objects with data from github - const extractedData: RoadmapData[] = items?.map((item: RoadmapData) => { - const fieldValueNodes = item?.fieldValues?.nodes; - const text = getFieldValueByName(fieldValueNodes, "Title"); - const startDate = getFieldValueByName(fieldValueNodes, "Start Date"); - const targetDate = getFieldValueByName( - fieldValueNodes, - "Target Date" - ); - const issueUrl = item?.content?.url; - - return { text, startDate, targetDate, issueUrl }; + const response = await fetch(`${endpoint}`, { + signal: abortController.signal, }); + const items = (await response.json()).nodes as unknown[]; + //creates an array of objects with data from github + const extractedData: RoadmapData[] = items?.map(mapRoadmapData); setRoadmapData(extractedData || []); } catch (error) { @@ -98,32 +80,78 @@ export const Roadmap = ({ title, children, endpoint }: RoadmapProps) => { } }; - fetchData(); + void fetchData(); + + return () => { + abortController.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const getFieldValueByName = (fieldValueNodes: any[], fieldName: string) => { - const fieldValueNode = fieldValueNodes?.find( - (node: any) => node?.field?.name === fieldName - ); - return ( - fieldValueNode?.text || fieldValueNode?.date || fieldValueNode?.name || "" - ); + const mapRoadmapData = (responseItem: any): RoadmapData => { + const data: RoadmapData = { + id: responseItem.id, + startSprint: null, + endSprint: null, + issueUrl: responseItem.content.url, + title: "", + quarter: null, + }; + for (const fieldValueNode of responseItem.fieldValues.nodes) { + if (!("field" in fieldValueNode)) { + continue; + } + switch (fieldValueNode.field.name) { + case "Title": { + data.title = fieldValueNode.text; + break; + } + case "Start Sprint": { + data.startSprint = { + title: fieldValueNode.title, + startDate: fieldValueNode.startDate, + duration: fieldValueNode.duration, + }; + break; + } + case "End Sprint": { + data.endSprint = { + title: fieldValueNode.title, + startDate: fieldValueNode.startDate, + duration: fieldValueNode.duration, + }; + break; + } + case "Quarter": { + data.quarter = { + title: fieldValueNode.title, + startDate: fieldValueNode.startDate, + duration: fieldValueNode.duration, + }; + break; + } + } + } + return data; }; return (
- - setSearchQuery((event.target as HTMLInputElement).value) - } - className={styles.searchInput} - startAdornment={} - /> + + Filter list + + setSearchQuery((event.target as HTMLInputElement).value) + } + className={styles.searchInput} + startAdornment={} + /> + {roadmapData !== null && roadmapData.length > 0 ? ( - + ) : ( { ); }; -interface ItemProps { - targetDate: string | number | Date; - id: Key | null | undefined; - issueUrl: string | undefined; - text: - | string - | number - | boolean - | React.ReactElement> - | React.ReactFragment - | React.ReactPortal - | null - | undefined; -} -const ColumnData: React.FC<{ item: ItemProps; future?: boolean }> = ({ +const ColumnData: React.FC<{ item: RoadmapData; future?: boolean }> = ({ item, future = true, }) => { - const formattedDate = formatDate(new Date(item.targetDate)); - return ( <> @@ -163,36 +175,39 @@ const ColumnData: React.FC<{ item: ItemProps; future?: boolean }> = ({ className={future ? styles.cardFuture : styles.cardInProgress} key={item.id} > - {item.text} - Due Date: -

{formattedDate}

+ {item.title} + {future ? ( + Scheduled for: {item.quarter?.title} + ) : ( + Target for sprint: {item.endSprint?.title} + )}
); }; -export const CardView = ({ data, searchQuery }: CardViewProps) => { - const futureData = data.filter((item) => { - const startDate = item.startDate ? new Date(item.startDate) : null; - const today = new Date(); - const isFutureItem = !startDate || startDate > today; - const matchesSearchQuery = - searchQuery === "" || - item.text.toLowerCase().includes(searchQuery.toLowerCase()); - return isFutureItem && matchesSearchQuery; - }); - - const inProgressData = data.filter((item) => { - const startDate = new Date(item.startDate); - const targetDate = new Date(item.targetDate); - const today = new Date(); - const isInRange = startDate <= today && today <= targetDate; - const matchesSearchQuery = - searchQuery === "" || - item.text.toLowerCase().includes(searchQuery.toLowerCase()); - return isInRange && matchesSearchQuery; - }); +interface CardViewProps { + data: RoadmapData[]; +} + +const CardView = ({ data }: CardViewProps) => { + const plannedData: RoadmapData[] = []; + const inProgressData: RoadmapData[] = []; + + for (const d of data) { + if (d.endSprint) { + const sprintDate = new Date(d.endSprint.startDate); + sprintDate.setDate(sprintDate.getDate() + d.endSprint.duration); + if (new Date() < sprintDate) { + inProgressData.push(d); + } else { + plannedData.push(d); + } + } else { + plannedData.push(d); + } + } return ( @@ -210,9 +225,11 @@ export const CardView = ({ data, searchQuery }: CardViewProps) => { In backlog - {futureData.map((item) => ( - - ))} + {plannedData + .sort((a, b) => a.quarter!.title.localeCompare(b.quarter!.title)) + .map((item) => ( + + ))}
); diff --git a/site/src/components/roadmap/style.module.css b/site/src/components/roadmap/style.module.css index c8f174b3089..23f4c246cd3 100644 --- a/site/src/components/roadmap/style.module.css +++ b/site/src/components/roadmap/style.module.css @@ -27,11 +27,6 @@ flex: 1; max-width: 400px; width: 100%; - margin-top: var(--saltCard-padding, var(--salt-size-container-spacing)); -} - -.date { - display: inline; } .column { diff --git a/site/src/pages/api/roadmap.ts b/site/src/pages/api/roadmap.ts index a88283c14c3..69aa594f4b8 100644 --- a/site/src/pages/api/roadmap.ts +++ b/site/src/pages/api/roadmap.ts @@ -18,12 +18,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { endCursor } nodes { + id content { ...on Issue { url } } - fieldValues(first: 10) { + fieldValues(first: 20) { nodes { ...on ProjectV2ItemFieldDateValue { date @@ -49,6 +50,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } } + ... on ProjectV2ItemFieldIterationValue { + title + startDate + duration + field { + ... on ProjectV2IterationField { + name + } + } + } } } } @@ -59,20 +70,36 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } `; - const response = await fetch(`${process.env.GRAPH_QL}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - }, - body: JSON.stringify({ - query, - variables: { org, repo, endCursor: "null" }, - }), - }); + let hasNextPage = true; + let endCursor = "null"; + const allProjectItems = []; + + while (hasNextPage) { + const response = await fetch(`${process.env.GRAPH_QL}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + }, + body: JSON.stringify({ + query, + variables: { org, repo, endCursor }, + }), + }); + + const responseData = await response.json(); + const projectItems = + responseData.data.organization.repository.projectV2.items; + + hasNextPage = projectItems.pageInfo.hasNextPage; + endCursor = projectItems.pageInfo.endCursor; + + allProjectItems.push(...projectItems.nodes); + } - const responseData = await response.json(); - res.status(200).json(responseData); + const response = { nodes: allProjectItems }; + console.log("Requested GitHub API", { allProjectItems }); + res.status(200).json(response); } catch (error) { console.error(error); res.status(404).json({ message: "Error fetching data" });