diff --git a/package-lock.json b/package-lock.json index fe995f4..32a62ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1833,6 +1833,14 @@ "@types/react": "*" } }, + "@types/react-csv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.1.tgz", + "integrity": "sha512-sP8AxGrFJ/kb7ygFpGkssdD/vKSTqdZDJbw3pJKTCa1C0UoT+0+rdUWy2fZqvhvvdpHG+OCJ4G8O7OZqVIa+BA==", + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "16.9.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz", @@ -2187,6 +2195,24 @@ } } }, + "ag-grid-community": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-22.1.1.tgz", + "integrity": "sha512-FNyv9e9JIuuR8NNi/r3NjIjUVy2/K5GgPjwQ63g9/Z4U8EudQZLINGMVKI6OwtZfWyyNSd0hQDCNsdvx0OR1WQ==" + }, + "ag-grid-enterprise": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/ag-grid-enterprise/-/ag-grid-enterprise-22.1.1.tgz", + "integrity": "sha512-TLZi9++lrxlCQY2SI+5b4VXDP2O/P0ssUQSelifA2CvwwGzQcPkPWZQKrIHlCkIHBuBoJ9OiEw7GX3AIuoBUug==" + }, + "ag-grid-react": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-22.1.1.tgz", + "integrity": "sha512-zMz2R3pyo2hEw+GUL0YcsrcMFPxcs7ZjMyhBJpqxfMT/EdwFEGupsrRHKZQjxTaEAgawDhx8Rzt8dNFBLNk3qw==", + "requires": { + "prop-types": "^15.6.2" + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -10884,6 +10910,11 @@ "universal-cookie": "^4.0.0" } }, + "react-csv": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.0.1.tgz", + "integrity": "sha512-GK2MVXvEQW1PxaDja2Ksyb487Ae8jC8Kq70vrqwl1KFQojQleK2q/nDxeExe8482jAVLOE+w1qCCZN5Gu5ekKA==" + }, "react-dev-utils": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.0.tgz", @@ -11017,6 +11048,11 @@ "scheduler": "^0.18.0" } }, + "react-dom-factories": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.2.tgz", + "integrity": "sha1-63cFxNs2+1AbOqOP91lhaqD/luA=" + }, "react-error-overlay": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.6.tgz", diff --git a/package.json b/package.json index b3a21de..e4c0f3f 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,18 @@ "@types/material-ui": "^0.21.7", "@types/node": "^12.12.27", "@types/react": "^16.9.20", + "@types/react-csv": "^1.1.1", "@types/react-dom": "^16.9.5", "@types/react-router-dom": "^5.1.3", + "ag-grid-community": "^22.1.1", + "ag-grid-enterprise": "^22.1.1", + "ag-grid-react": "^22.1.1", "axios": "^0.19.2", "react": "^16.12.0", "react-cookie": "^4.0.3", + "react-csv": "^2.0.1", "react-dom": "^16.12.0", + "react-dom-factories": "^1.0.2", "react-router-dom": "^5.1.2", "react-scripts": "3.4.0", "typescript": "^3.7.5" diff --git a/src/App.tsx b/src/App.tsx index 12ddded..934538e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; +import Issue from "./containers/Issue"; import Issues from "./containers/Issues"; import Home from './containers/Home'; @@ -60,6 +61,9 @@ const App: React.FC = () => { + + + diff --git a/src/constants/Issues.ts b/src/constants/Issues.ts new file mode 100644 index 0000000..8e6ed52 --- /dev/null +++ b/src/constants/Issues.ts @@ -0,0 +1,11 @@ +export const HEADERS = [ + { label: "Issue Number", key: "number" }, + { label: "Issue Title", key: "title" }, + { label: "Description", key: "body" }, + { label: "Reported By", key: "reporter" }, + { label: "Issue Type", key: "issueType" }, + { label: "Issue Status", key: "issueStatus" }, + { label: "Component", key: "component" } +]; +export const ISSUE_TYPES = ["bug", "requirement"]; +export const ISSUE_STATUS = ["low", "high", "medium", "critical"]; \ No newline at end of file diff --git a/src/containers/Home.tsx b/src/containers/Home.tsx index e9aeb3a..524afea 100644 --- a/src/containers/Home.tsx +++ b/src/containers/Home.tsx @@ -5,6 +5,10 @@ import GitHubIcon from '@material-ui/icons/GitHub'; import { useLocation } from "react-router-dom"; import axios from 'axios'; import { Octokit } from "@octokit/rest"; + +import { + Link + } from "react-router-dom"; interface Props { cookies: any; } @@ -93,16 +97,20 @@ const Home: React.FC = ({ cookies }) => { const renderHome = () => { return ( -

Welcom Home, {user.name}

+
+

Welcom Home, {user.name}

+ + Issues +
) } - if (!code && !cookeieToken) { - return renderLoginButton(); - } +if (!code && !cookeieToken) { + return renderLoginButton(); +} - return renderHome(); +return renderHome(); } diff --git a/src/containers/Issue.tsx b/src/containers/Issue.tsx new file mode 100644 index 0000000..5f39a80 --- /dev/null +++ b/src/containers/Issue.tsx @@ -0,0 +1,187 @@ +import React, { useEffect, useState } from 'react' +import { withCookies } from 'react-cookie'; +import { Octokit } from "@octokit/rest"; +import { CSVDownload } from "react-csv"; +import { HEADERS, ISSUE_TYPES, ISSUE_STATUS } from "../constants/Issues"; +import { AgGridReact } from 'ag-grid-react'; +import 'ag-grid-community/dist/styles/ag-grid.css'; +import 'ag-grid-community/dist/styles/ag-theme-balham.css'; + +import { + BrowserRouter as Router, + Link, + useLocation +} from "react-router-dom"; + +interface Props { + cookies: any; +} + +interface Issue { + number: number; + title: string; + state:string; + body: string; + reporter: string; + issueType: string; + issueStatus: string; + component: string; + createdAt: string; + updatedAt: string; + closedAt: string; +} + +const Issue: React.FC = ({ cookies }) => { + + let cookeieToken = cookies.get("token"); + cookeieToken = cookeieToken === "undefined" ? undefined : cookeieToken; + let issuesArr: Issue[] = [], + octokit: Octokit = new Octokit({ + auth: cookeieToken + }); + const columnDefs = [ + { headerName: "Issue Number", field: "number" }, + { headerName: "Issue Title", field: "title" }, + { headerName: "Issue Status", field: "state" }, + { headerName: "Description", field: "body" }, + { headerName: "Reported By", field: "reporter" }, + { headerName: "Issue Type", field: "issueType" }, + { headerName: "Issue Status", field: "issueStatus" }, + { headerName: "Component", field: "component" }, + { headerName: "Created At", field: "createdAt" }, + { headerName: "updated At", field: "updatedAt" }, + { headerName: "Closed At", field: "closedAt" }, + + ]; + + let gridOptions: any = null; + + const [issues, setIssues] = useState({ + rowData: issuesArr, + columnDefs: columnDefs, + gridOptions: gridOptions + }); + + + // A custom hook that builds on useLocation to parse + // the query string for you. + const useQuery = () => { + return new URLSearchParams(useLocation().search); + } + + let query = useQuery(); + + let rowData = [ + { make: "Toyota", model: "Celica", price: 35000 }, + { make: "Ford", model: "Mondeo", price: 32000 }, + { make: "Porsche", model: "Boxter", price: 72000 } + ]; + + useEffect(() => { + const getRepoData = async () => { + + let repoFullName = query.get('full_name'); + + if (octokit !== undefined && repoFullName !== null) { + + let result: Issue[] = []; + let hasNextPage = true; + let currPage = 1; + let pageSize = 100; + while (hasNextPage) { + + const repoFullNameArr = repoFullName.split("/"), + response = await octokit.issues.listForRepo({ + owner: repoFullNameArr[0], + repo: repoFullNameArr[1], + state: "all", + sort: "updated", + direction: "desc", + per_page: pageSize, + page: currPage + }); + console.log("Issue API Response ", response, "currPage ", currPage, " pageSize", pageSize); + if (response && response.status === 200) { + if (response.data.length < pageSize) { + + hasNextPage = false; + } { + currPage++ + } + response.data.forEach(data => { + let issueType = "", + issueStatus = "", + component = ""; + + if (data.labels) { + data.labels.forEach((label) => { + if (label.name.toLowerCase().indexOf("xviz") !== -1) { + component = label.name; + } + if (ISSUE_STATUS.indexOf(label.name.toLowerCase()) !== -1) { + issueStatus = label.name; + } + if (ISSUE_TYPES.indexOf(label.name.toLowerCase()) !== -1) { + issueType = label.name; + } + }) + } + + result.push({ + number: data.number, + title: data.title, + state: data.state, + body: data.body, + reporter: data.user.login, + issueType: issueType, + issueStatus: issueStatus, + component: component, + createdAt: data.created_at ? new Date(data.created_at).toLocaleDateString() : "", + updatedAt: data.updated_at ? new Date(data.updated_at).toLocaleDateString() : "", + closedAt: data.closed_at ? new Date(`${data.closed_at}`).toLocaleDateString() : "" + }) + }); + } else { + hasNextPage = false; + } + + } + + setIssues({ + rowData: result, + columnDefs: columnDefs, + gridOptions: issues.gridOptions + }) + + } + } + + getRepoData(); + // eslint-disable-next-line + }, []); + return ( +
+ + { issues.gridOptions = e; }}> + + +
+ ) +} + +export default withCookies(Issue) \ No newline at end of file diff --git a/src/containers/Issues.tsx b/src/containers/Issues.tsx index 8a1d11c..4693c89 100644 --- a/src/containers/Issues.tsx +++ b/src/containers/Issues.tsx @@ -1,16 +1,184 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react' +import { withCookies } from 'react-cookie'; +import { Octokit } from "@octokit/rest"; +import { CSVDownload } from "react-csv"; +import { HEADERS, ISSUE_TYPES, ISSUE_STATUS } from "../constants/Issues" +import { + BrowserRouter as Router, + Link, + useLocation + } from "react-router-dom"; interface Props { + cookies: any; } -const Issues: React.FC = () => { +interface Repo { + name: string; + private: boolean; + full_name: string; + description: string; + url: string; +} + +interface Issue { + number: number; + title: string; + body: string; + reporter: string; + issueType: string; + issueStatus: string; + component: string; +} + +const Issues: React.FC = ({ cookies }) => { + + let cookeieToken = cookies.get("token"); + cookeieToken = cookeieToken === "undefined" ? undefined : cookeieToken; + let repos: Repo[] = [], + octokit: Octokit = new Octokit({ + auth: cookeieToken + }); + + const [issues, setIssues] = useState({ + repos: repos + }); + + const downloadIssue: any = async (repoName: string, repoFullName: string) => { + + if (octokit !== undefined) { + + let result: Issue[] = []; + + const repoFullNameArr = repoFullName.split("/"), + response = await octokit.issues.listForRepo({ + owner: repoFullNameArr[0], + repo: repoFullNameArr[1], + state: "all", + sort: "updated", + direction: "desc", + per_page: 100, + page: 1 + }); + + console.log(repoFullName, response); + + if (response && response.status === 200) { + response.data.forEach(data => { + let issueType = "", + issueStatus = "", + component = ""; + if (data.labels) { + data.labels.forEach((label) => { + if (label.name.toLowerCase().indexOf("xviz") !== -1) { + component = label.name; + } + if (ISSUE_STATUS.indexOf(label.name.toLowerCase()) !== -1) { + issueStatus = label.name; + } + if (ISSUE_TYPES.indexOf(label.name.toLowerCase()) !== -1) { + issueType = label.name; + } + }) + } + + result.push({ + number: data.number, + title: data.title, + body: data.body, + reporter: data.user.login, + issueType: issueType, + issueStatus: issueStatus, + component: component + }) + }); + } + return ( + + Download Issues + + ); + + } else { + return null; + } + + }; + + const getRepoTable = (repos: Repo[]) => { + + let table: any[] = []; + + { + repos.forEach(repo => { + table.push( + + {repo.name} + {repo.full_name} + {repo.description} + + ) + }) + } + + return table; + } + + useEffect(() => { + const getRepoData = async () => { + if (cookeieToken && octokit) { + const reposResp = await octokit.repos.list({ per_page: 100, page: 1 }); + console.log("repos", issues); + if (reposResp && reposResp.status === 200) { + + reposResp.data.forEach((repoData: any) => { + repos.push({ + name: repoData.name, + private: repoData.private, + full_name: repoData.full_name, + description: repoData.description, + url: repoData.html_url + }) + }); + } + + setIssues({ + repos: repos + }) + } + } + + getRepoData(); + // eslint-disable-next-line + }, []); return (
- Issues + + + + + + + + + + + {getRepoTable(issues.repos)} + +
+ Name + + URL + + Description + + Export +
+
) } -export default Issues; +export default withCookies(Issues) \ No newline at end of file