diff --git a/nerdlets/maintainer-dashboard/components/dashboard.js b/nerdlets/maintainer-dashboard/components/dashboard.js
new file mode 100644
index 0000000..97aa6b6
--- /dev/null
+++ b/nerdlets/maintainer-dashboard/components/dashboard.js
@@ -0,0 +1,146 @@
+import React from 'react';
+import {
+ Card,
+ CardHeader,
+ CardBody,
+ HeadingText,
+ Spinner,
+ Grid,
+ GridItem,
+ Stack,
+ StackItem,
+ Spacing,
+ Table,
+ TableHeader,
+ TableHeaderCell,
+ TableRow,
+ TableRowCell,
+ Button,
+ Tabs,
+ TabsItem,
+ BillboardChart,
+ BlockText,
+ Icon,
+ Modal,
+ TextField,
+ Link,
+ Select,
+ SelectItem,
+ UserStorageMutation,
+} from 'nr1';
+import { KNOWN_LABEL_COLORS } from './issueLabel';
+import SettingsUI from './settings';
+import DashboardData from './dashboardData';
+import UserSettingsQuery from '../util/storageUtil';
+import { client } from '../graphql/ApolloClientInstance';
+import NewRelicUsers from '../data/userdata-sample.json';
+
+const RELICS = Object.values(NewRelicUsers)
+ .filter((u) => u.user_type === 'relic' || u.user_type === 'contractor')
+ .map((u) => u.login)
+ .sort();
+
+export default class MaintainerDashboard extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ queryKey: 0,
+ settingsHidden: true,
+ };
+ }
+
+ render() {
+ return (
+
+
+
+ {({ loading, token, settings }) => {
+ if (loading)
+ return ;
+ // create apollo client and settings UI
+ const gqlClient = client(token);
+ // return settings selection front and center if we have no settings
+ if (!token || !settings)
+ return (
+ ({ name, color }))}
+ onSubmit={() =>
+ this.setState(({ queryKey }) => ({
+ settingsHidden: true,
+ queryKey: queryKey + 1,
+ }))
+ }
+ client={gqlClient}
+ style={{ maxWidth: '60em' }}
+ />
+ );
+ return (
+ // else return the dashboard with a settings modal
+ <>
+
+ this.setState({ settingsHidden: true })}
+ onHideEnd={() =>
+ this.setState(({ queryKey }) => ({
+ queryKey: queryKey + 1,
+ }))
+ }
+ >
+ ({ name, color }))}
+ onSubmit={() =>
+ this.setState(() => ({
+ settingsHidden: true,
+ }))
+ }
+ client={gqlClient}
+ />
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }}
+
+
+
+ );
+ }
+}
diff --git a/nerdlets/maintainer-dashboard/dashboard.js b/nerdlets/maintainer-dashboard/components/dashboardData.js
similarity index 92%
rename from nerdlets/maintainer-dashboard/dashboard.js
rename to nerdlets/maintainer-dashboard/components/dashboardData.js
index 0f78f16..38bb2ee 100644
--- a/nerdlets/maintainer-dashboard/dashboard.js
+++ b/nerdlets/maintainer-dashboard/components/dashboardData.js
@@ -1,14 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as humanizeDuration from 'humanize-duration';
-import BootstrapTable from 'react-bootstrap-table-next';
-import filterFactory, {
- textFilter,
- selectFilter,
- dateFilter,
- Comparator,
- multiSelectFilter,
-} from 'react-bootstrap-table2-filter';
import {
Spinner,
Stack,
@@ -18,7 +10,6 @@ import {
TableHeaderCell,
TableRow,
TableRowCell,
- Button,
Tabs,
TabsItem,
BillboardChart,
@@ -26,10 +17,12 @@ import {
Link,
Tooltip,
} from 'nr1';
-import { getGithubData } from './githubData';
+import { findDashboardItems } from '../graphql/githubData';
import { IssueTable } from './issueTable';
-export default class Dashboard extends React.Component {
+// TODO: figure out how to fix the tab labels from duplicating the key
+
+export default class DashboardData extends React.Component {
static propTypes = {
client: PropTypes.object,
scanRepos: PropTypes.arrayOf(PropTypes.string),
@@ -52,7 +45,7 @@ export default class Dashboard extends React.Component {
async componentDidMount() {
// send to UI
this.setState(
- await getGithubData(this.props.client, {
+ await findDashboardItems(this.props.client, {
scanRepos: this.props.scanRepos,
companyUsers: this.props.companyUsers.concat(this.props.ignoreUsers),
ignoreLabels: this.props.ignoreLabels,
diff --git a/nerdlets/maintainer-dashboard/issueLabel.js b/nerdlets/maintainer-dashboard/components/issueLabel.js
similarity index 100%
rename from nerdlets/maintainer-dashboard/issueLabel.js
rename to nerdlets/maintainer-dashboard/components/issueLabel.js
diff --git a/nerdlets/maintainer-dashboard/issueTable.js b/nerdlets/maintainer-dashboard/components/issueTable.js
similarity index 97%
rename from nerdlets/maintainer-dashboard/issueTable.js
rename to nerdlets/maintainer-dashboard/components/issueTable.js
index f3a8eda..203d397 100644
--- a/nerdlets/maintainer-dashboard/issueTable.js
+++ b/nerdlets/maintainer-dashboard/components/issueTable.js
@@ -11,8 +11,8 @@ import filterFactory, {
} from 'react-bootstrap-table2-filter';
import { Button, Icon } from 'nr1';
import { IssueLabel } from './issueLabel';
-import PullRequestLogo from './img/git-pull-request-16.svg';
-import IssueLogo from './img/issue-opened-16.svg';
+import PullRequestLogo from '../img/git-pull-request-16.svg';
+import IssueLogo from '../img/issue-opened-16.svg';
/** */
export class IssueTable extends React.PureComponent {
diff --git a/nerdlets/maintainer-dashboard/settings.js b/nerdlets/maintainer-dashboard/components/settings.js
similarity index 91%
rename from nerdlets/maintainer-dashboard/settings.js
rename to nerdlets/maintainer-dashboard/components/settings.js
index c46019f..0117b30 100644
--- a/nerdlets/maintainer-dashboard/settings.js
+++ b/nerdlets/maintainer-dashboard/components/settings.js
@@ -1,5 +1,4 @@
import React from 'react';
-import gql from 'graphql-tag';
import {
HeadingText,
Stack,
@@ -14,24 +13,8 @@ import {
} from 'nr1';
import { Multiselect } from 'react-widgets';
import { IssueLabel } from './issueLabel';
-import UserSettingsQuery from './storageUtil';
-
-const GET_CUR_USER_INFO = gql`
- query($repoCursor: String) {
- viewer {
- login
- repositories(
- affiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]
- first: 100
- after: $repoCursor
- ) {
- nodes {
- nameWithOwner
- }
- }
- }
- }
-`;
+import { getUserInfo } from '../graphql/githubData';
+import UserSettingsQuery from '../util/storageUtil';
export default class SettingsUI extends React.Component {
static splitRepositoryNames(searchTerm) {
@@ -105,49 +88,39 @@ export default class SettingsUI extends React.Component {
});
// test the token with a user information query
const curNum = ++this.curFetchIndex;
- return this.props.client
- .query({
- query: GET_CUR_USER_INFO,
- fetchPolicy: 'network-only',
- context: {
- headers: {
- authorization: `Bearer ${token}`,
+ try {
+ const data = await getUserInfo(this.props.client, this.state.token);
+ // prevent race conditions
+ if (curNum === this.curFetchIndex) {
+ this.setState({
+ patStatus: {
+ valid: true,
+ userName: data?.viewer?.login,
+ repoOptions: data?.viewer?.repositories?.nodes?.map?.(
+ ({ nameWithOwner }) => nameWithOwner
+ ),
},
- },
- })
- .then(({ data }) => {
- // prevent race conditions
- if (curNum === this.curFetchIndex) {
+ });
+ }
+ } catch (e) {
+ // prevent race condition
+ if (curNum === this.curFetchIndex) {
+ if (e?.networkError?.statusCode === 401)
this.setState({
patStatus: {
- valid: true,
- userName: data?.viewer?.login,
- repoOptions: data?.viewer?.repositories?.nodes?.map?.(
- ({ nameWithOwner }) => nameWithOwner
- ),
+ valid: false,
+ message: 'Token returned authorization error',
},
});
- }
- })
- .catch((e) => {
- // prevent race condition
- if (curNum === this.curFetchIndex) {
- if (e?.networkError?.statusCode === 401)
- this.setState({
- patStatus: {
- valid: false,
- message: 'Token returned authorization error',
- },
- });
- else
- this.setState({
- patStatus: {
- valid: false,
- message: 'Unknown GitHub API error',
- },
- });
- }
- });
+ else
+ this.setState({
+ patStatus: {
+ valid: false,
+ message: 'Unknown GitHub API error',
+ },
+ });
+ }
+ }
}
handlePATToken(event) {
diff --git a/nerdlets/maintainer-dashboard/githubData.js b/nerdlets/maintainer-dashboard/githubData.js
deleted file mode 100644
index 7cc1498..0000000
--- a/nerdlets/maintainer-dashboard/githubData.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- SEARCH_NEW_ITEMS_QUERY,
- SEARCH_STALE_ITEMS_QUERY,
- makeDefStaleSearch,
- makeMaybeStaleSearch,
- makeNewSearch,
-} from './graphql/GithubQueries';
-
-/**
- * Run a GraphQL query to get information about new and stale items for a given
- * set of repositories. TODO: better overview of new/stale items
- *
- * @param {any} client Apollo GraphQL client to use to query the GitHub GraphQL
- * API. Must be preloaded with the proper credentials.
- * @param {string[]} options.scanRepos The repositories to scan, in "org/name"
- * format.
- * @param {string[]} options.companyUsers Login names of GitHub accounts
- * associated with employees. This value is used to determine which items
- * have received a response from someone in the company.
- * @param {string[]} options.ignoreLabels Issue/PR labels to exclude. All
- * issues/PRs with these labels will be ignored.
- * @param {number} options.staleTime Duration in milliseconds that a item should
- * remain inactive for it to be considered stale.
- * @returns {object} TODO: more docs
- */
-export async function getGithubData(client, options) {
- const { scanRepos, companyUsers, staleTime, ignoreLabels } = options;
- const staleDate = new Date(Date.now() - staleTime);
- // fetch all the data
- const [newRes, staleRes] = await Promise.all([
- client.query({
- query: SEARCH_NEW_ITEMS_QUERY,
- variables: {
- query: makeNewSearch(companyUsers, scanRepos, ignoreLabels),
- },
- }),
- client.query({
- query: SEARCH_STALE_ITEMS_QUERY,
- variables: {
- queryDefStale: makeDefStaleSearch(
- companyUsers,
- scanRepos,
- ignoreLabels,
- staleDate
- ),
- queryMaybeStale: makeMaybeStaleSearch(
- companyUsers,
- scanRepos,
- ignoreLabels,
- staleDate
- ),
- timeSince: staleDate.toISOString(),
- },
- }),
- ]);
- // filter the stale data to only the actually stale items
- const nrSet = new Set(companyUsers);
- // if every comment by a relic is stale, then the issue is stale
- // TODO: filter timeline events for interactions
- const filteredMaybeItems = staleRes.data.maybeStale.nodes.filter((n) =>
- n.timelineItems.nodes.every((c) =>
- c?.author && nrSet.has(c.author.login)
- ? new Date(c.updatedAt) <= staleDate
- : true
- )
- );
- return {
- newSearchCount: Math.max(
- newRes.data.search.issueCount,
- newRes.data.search.nodes.length
- ),
- newSearchItems: newRes.data.search.nodes,
- staleSearchCount:
- Math.max(
- staleRes.data.definitelyStale.issueCount,
- staleRes.data.definitelyStale.nodes.length
- ) + filteredMaybeItems.length,
- staleSearchItems: staleRes.data.definitelyStale.nodes.concat(
- filteredMaybeItems
- ),
- };
-}
diff --git a/nerdlets/maintainer-dashboard/graphql/ApolloClientInstance.js b/nerdlets/maintainer-dashboard/graphql/ApolloClientInstance.js
index 414eea5..e9eec7e 100644
--- a/nerdlets/maintainer-dashboard/graphql/ApolloClientInstance.js
+++ b/nerdlets/maintainer-dashboard/graphql/ApolloClientInstance.js
@@ -23,7 +23,7 @@ const GITHUB_BASE_URL = 'https://api.github.com/graphql';
export const client = (userToken) => {
return new ApolloClient({
link: ApolloLink.from([
- // new RetryLink(),
+ new RetryLink(),
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.error('GRAPHQL_ERR::>', graphQLErrors);
diff --git a/nerdlets/maintainer-dashboard/graphql/ErrorMessage.js b/nerdlets/maintainer-dashboard/graphql/ErrorMessage.js
deleted file mode 100644
index 4fe290d..0000000
--- a/nerdlets/maintainer-dashboard/graphql/ErrorMessage.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-/**
- * Renders error content for GraphQL API calls Borrowed from:
- * https://github.com/the-road-to-graphql/react-graphql-github-apollo/blob/mast
- * r/src/Error/index.js
- */
-const ErrorMessage = ({ error }) => (
-
- {error.toString()}
-
-);
-
-export default ErrorMessage;
diff --git a/nerdlets/maintainer-dashboard/graphql/GithubQueries.js b/nerdlets/maintainer-dashboard/graphql/GithubQueries.js
deleted file mode 100644
index ba43a6d..0000000
--- a/nerdlets/maintainer-dashboard/graphql/GithubQueries.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import gql from 'graphql-tag';
-
-// TODO: split query into search + virtualized table to improve caching
-/**
- * Fragment indicating the values and structure of all issue objects fetched
- * using the github queries below.
- */
-const ISSUE_FRAGMENT = gql`
- fragment GetIssueInfo on Issue {
- id
- title
- author {
- login
- url
- }
- repository {
- name
- url
- }
- labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
- nodes {
- name
- color
- }
- }
- number
- url
- createdAt
- }
-`;
-
-/**
- * Fragment indicating the values and structure of all PR objects fetched using
- * the github queries below.
- */
-const PR_FRAGMENT = gql`
- fragment GetPRInfo on PullRequest {
- id
- title
- author {
- login
- url
- }
- repository {
- name
- url
- }
- labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
- nodes {
- name
- color
- }
- }
- number
- url
- createdAt
- }
-`;
-
-/**
- * Query to perform a GitHub search and fetch all "Issues" (Issues and PRs) that
- * match a given query. We can use this graphQL query and GitHub's powerful
- * search syntax to only fetch items which were created by users who are not
- * employees. Use makeNewSearch to construct a search query paramter that finds
- * PRs and Issues that are open and not created/commented by an employee.
- *
- * @param {string} query The GitHub search query to search with.
- */
-export const SEARCH_NEW_ITEMS_QUERY = gql`
- query SearchResults($query: String!) {
- search(query: $query, type: ISSUE, first: 100) {
- nodes {
- __typename
- ...GetIssueInfo
- ...GetPRInfo
- }
- issueCount
- }
- rateLimit {
- limit
- cost
- remaining
- resetAt
- }
- }
- ${PR_FRAGMENT}
- ${ISSUE_FRAGMENT}
-`;
-
-export const SEARCH_STALE_ITEMS_QUERY = gql`
- query SearchResults(
- $queryDefStale: String!
- $queryMaybeStale: String!
- $timeSince: DateTime!
- ) {
- definitelyStale: search(query: $queryDefStale, type: ISSUE, first: 100) {
- issueCount
- nodes {
- __typename
- ...GetIssueInfo
- ...GetPRInfo
- }
- }
- maybeStale: search(query: $queryMaybeStale, type: ISSUE, first: 100) {
- issueCount
- nodes {
- __typename
- ...GetIssueInfo
- ...GetPRInfo
- ... on Issue {
- timelineItems(since: $timeSince, last: 100) {
- nodes {
- ... on Comment {
- id
- author {
- login
- }
- updatedAt
- }
- }
- }
- }
- ... on PullRequest {
- timelineItems(since: $timeSince, last: 100) {
- nodes {
- ... on Comment {
- id
- author {
- login
- }
- updatedAt
- }
- }
- }
- }
- }
- }
- rateLimit {
- limit
- cost
- remaining
- resetAt
- }
- }
- ${PR_FRAGMENT}
- ${ISSUE_FRAGMENT}
-`;
-
-export function makeNewSearch(users, repos, ignoreLabels) {
- return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
- .map((u) => `-author:${u} -commenter:${u}`)
- .join(' ')} ${ignoreLabels.map((l) => `-label:${l}`).join(' ')} is:open`;
-}
-
-export function makeDefStaleSearch(users, repos, ignoreLabels, date) {
- return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
- .map((u) => `-author:${u} commenter:${u}`)
- .join(' ')} ${ignoreLabels
- .map((l) => `-label:${l}`)
- .join(' ')} is:open updated:<=${date.toISOString()}`;
-}
-
-export function makeMaybeStaleSearch(users, repos, ignoreLabels, date) {
- return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
- .map((u) => `-author:${u} commenter:${u}`)
- .join(' ')} ${ignoreLabels
- .map((l) => `-label:${l}`)
- .join(
- ' '
- )} is:open updated:>${date.toISOString()} created:<=${date.toISOString()}`;
-}
diff --git a/nerdlets/maintainer-dashboard/graphql/githubData.js b/nerdlets/maintainer-dashboard/graphql/githubData.js
new file mode 100644
index 0000000..4e84baa
--- /dev/null
+++ b/nerdlets/maintainer-dashboard/graphql/githubData.js
@@ -0,0 +1,413 @@
+import gql from 'graphql-tag';
+
+// TODO: split query into search + virtualized table to improve caching
+// TODO: implement query pagination to allow getting more than 100 items
+// TODO: more precise filtering of the timeline items (only checking for comments right now)
+
+/**
+ * Fragment indicating the values and structure of all issue objects fetched
+ * using the github queries below.
+ */
+const ISSUE_FRAGMENT = gql`
+ fragment GetIssueInfo on Issue {
+ id
+ title
+ author {
+ login
+ url
+ }
+ repository {
+ name
+ url
+ }
+ labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
+ nodes {
+ name
+ color
+ }
+ }
+ number
+ url
+ createdAt
+ }
+`;
+
+/**
+ * Fragment indicating the values and structure of all PR objects fetched using
+ * the github queries below.
+ */
+const PR_FRAGMENT = gql`
+ fragment GetPRInfo on PullRequest {
+ id
+ title
+ author {
+ login
+ url
+ }
+ repository {
+ name
+ url
+ }
+ labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
+ nodes {
+ name
+ color
+ }
+ }
+ number
+ url
+ createdAt
+ }
+`;
+
+/**
+ * Query to perform a GitHub search and fetch all "Issues" (Issues and PRs) that
+ * match a given query. We can use this graphQL query and GitHub's powerful
+ * search syntax to only fetch items which were created by users who are not
+ * employees. Use makeNewSearch to construct a search query paramter that finds
+ * PRs and Issues that are open and not created/commented by an employee.
+ *
+ * Note that due to the nature of the typically large search query, this GraphQL
+ * query may take a few seconds to execute.
+ *
+ * @param {string} query The GitHub search query to search with.
+ */
+const SEARCH_NEW_ITEMS_QUERY = gql`
+ query SearchResults($query: String!) {
+ search(query: $query, type: ISSUE, first: 100) {
+ nodes {
+ __typename
+ ...GetIssueInfo
+ ...GetPRInfo
+ }
+ issueCount
+ }
+ rateLimit {
+ limit
+ cost
+ remaining
+ resetAt
+ }
+ }
+ ${PR_FRAGMENT}
+ ${ISSUE_FRAGMENT}
+`;
+
+/**
+ * Query to perform a GitHub search and fetch all "Issues" (Issues and PRs) that
+ * match several given queries. We can use this graphQL query and GitHub's
+ * powerful search syntax to only fetch items which are considered stale.
+ *
+ * In this case, a "stale" item is an item that received a response from an
+ * employee but then received no other response from an employee for at least a
+ * certain duration. GitHub's search syntax doesn't quite allow us to get all
+ * this information in a single search, so the query is split into two separate
+ * searches:
+ *
+ * * definitelyStale - Issues or PRs that were not created by an employee, have
+ * at least one comment from an employee, and were last updated before the
+ * stale threshold. "Last updated" in GitHub terms can mean a variety of things
+ * (including comments from other users), so this query doesn't catch issues or
+ * PRs that are active but haven't received an employee response in awhile. The
+ * query for this search is generated by makeDefStaleSearch.
+ *
+ * * maybeStale - Issues or PRs that were not created by a an employee, and were
+ * created before the stale threshold but updated after it. As GitHub search
+ * cannot determine if an employee has commented within a certain time, this
+ * search fetches the TimeLine items of each issue/pr candidate, expecting the
+ * application to use this data to determine which Issues/PRs have received
+ * employee follow up. The query for this search is generated by
+ * makeDefStaleSearch, and the results from this search can be filtered using
+ * filterMaybeStaleItems.
+ *
+ * Note that due to the nature of the typically large search query, this GraphQL
+ * query may take a few seconds to execute.
+ *
+ * @param {string} queryDefStale The GitHub search query to search for
+ * definitelyStale items. Use makeDefStaleSearch to make this query.
+ * @param {string} queryMaybeStale The GitHub search query to search for
+ * maybeStale items. Use makeMaybeStaleSearch to make this query.
+ * @param {string} timeSince An ISO Date string indicating the threshold to use
+ * for considering items to be stale (ex. two weeks before today).
+ */
+const SEARCH_STALE_ITEMS_QUERY = gql`
+ query SearchResults(
+ $queryDefStale: String!
+ $queryMaybeStale: String!
+ $timeSince: DateTime!
+ ) {
+ definitelyStale: search(query: $queryDefStale, type: ISSUE, first: 100) {
+ issueCount
+ nodes {
+ __typename
+ ...GetIssueInfo
+ ...GetPRInfo
+ }
+ }
+ maybeStale: search(query: $queryMaybeStale, type: ISSUE, first: 100) {
+ issueCount
+ nodes {
+ __typename
+ ...GetIssueInfo
+ ...GetPRInfo
+ ... on Issue {
+ timelineItems(since: $timeSince, last: 100) {
+ nodes {
+ ... on Comment {
+ id
+ author {
+ login
+ }
+ updatedAt
+ }
+ }
+ }
+ }
+ ... on PullRequest {
+ timelineItems(since: $timeSince, last: 100) {
+ nodes {
+ ... on Comment {
+ id
+ author {
+ login
+ }
+ updatedAt
+ }
+ }
+ }
+ }
+ }
+ }
+ rateLimit {
+ limit
+ cost
+ remaining
+ resetAt
+ }
+ }
+ ${PR_FRAGMENT}
+ ${ISSUE_FRAGMENT}
+`;
+
+/**
+ * Utility function to create a search query that looks for items that haven't
+ * received an employee response yet. See SEARCH_NEW_ITEMS_QUERY for more
+ * information.
+ *
+ * @param {string[]} users GitHub logins (usernames, not emails) to treat as
+ * employees.
+ * @param {string[]} repos Repositories to pull Issues and PRs from.
+ * @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
+ * Issues and PRs with any of these labels will be excluded from results.
+ */
+
+function makeNewSearch(users, repos, ignoreLabels) {
+ return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
+ .map((u) => `-author:${u} -commenter:${u}`)
+ .join(' ')} ${ignoreLabels.map((l) => `-label:${l}`).join(' ')} is:open`;
+}
+
+/**
+ * Utility function to create a search query that looks for items that are
+ * definitely stale. See SEARCH_STALE_ITEMS_QUERY for more information.
+ *
+ * @param {string[]} users GitHub logins (usernames, not emails) to treat as
+ * employees.
+ * @param {string[]} repos Repositories to pull Issues and PRs from.
+ * @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
+ * Issues and PRs with any of these labels will be excluded from results.
+ * @param {Date} date A Date to treat as the threshold to considering an item to
+ * be stale (ex. two weeks before today).
+ */
+function makeDefStaleSearch(users, repos, ignoreLabels, date) {
+ return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
+ .map((u) => `-author:${u} commenter:${u}`)
+ .join(' ')} ${ignoreLabels
+ .map((l) => `-label:${l}`)
+ .join(' ')} is:open updated:<=${date.toISOString()}`;
+}
+
+/**
+ * Utility function to create a search query that looks for items that might be
+ * stale. See SEARCH_STALE_ITEMS_QUERY for more information.
+ *
+ * @param {string[]} users GitHub logins (usernames, not emails) to treat as
+ * employees.
+ * @param {string[]} repos Repositories to pull Issues and PRs from.
+ * @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
+ * Issues and PRs with any of these labels will be excluded from results.
+ * @param {Date} date A Date to treat as the threshold to considering an item to
+ * be stale (ex. two weeks before today).
+ */
+function makeMaybeStaleSearch(users, repos, ignoreLabels, date) {
+ return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
+ .map((u) => `-author:${u} commenter:${u}`)
+ .join(' ')} ${ignoreLabels
+ .map((l) => `-label:${l}`)
+ .join(
+ ' '
+ )} is:open updated:>${date.toISOString()} created:<=${date.toISOString()}`;
+}
+
+/**
+ * Filter SEARCH_STALE_ITEMS_QUERY.maybeStale.nodes based on which users are
+ * considered employees.
+ *
+ * @param {{ timelineItems: { nodes: any[] } }[]} items An array of Issues/PRs
+ * with a timeline
+ * @param {string[]} employeeLogins A list of GitHub logins to treat as
+ * employees
+ * @param {Date} staleDate A date to treat as the threshold of being stale (ex.
+ * two weeks before today).
+ * @returns {{ timelineItems: { nodes: any[] } }[]} The items paramter array
+ * with the stale Issues/PRs removed.
+ */
+function filterMaybeStaleItems(items, employeeLogins, staleDate) {
+ const employeeSet = new Set(employeeLogins);
+ return items.filter((n) =>
+ n.timelineItems.nodes.every((c) =>
+ c?.author && employeeSet.has(c.author.login)
+ ? new Date(c.updatedAt) <= staleDate
+ : true
+ )
+ );
+}
+
+/**
+ * Get some useful information about the user we are currently authenticated as.
+ * This query is used to both test the authentication mechanism and fetch
+ * information about the account that can be used to provide helpful
+ * suggestions in the settings.
+ *
+ * @param {?string} repoCursor An endCursor to paginate over the repositories
+ * with.
+ */
+const GET_CUR_USER_INFO_QUERY = gql`
+ query($repoCursor: String) {
+ viewer {
+ login
+ repositories(
+ affiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]
+ first: 100
+ after: $repoCursor
+ ) {
+ nodes {
+ nameWithOwner
+ }
+ }
+ }
+ }
+`;
+
+/**
+ * Run a GraphQL query to get information about new and stale items for a given
+ * set of repositories. In this case, new items are Issues/PRs that have not
+ * received a response from an employee, and stale items are Issues/PRs that
+ * have received an employee response but have not received a follow up for
+ * longer than a certain period of time. For more information about the exact
+ * process used to determine which item is new/stale, check out the
+ * SEARCH_NEW_ITEMS_QUERY and SEARCH_STALE_ITEMS_QUERY queries.
+ *
+ * Note: this function currently is limited to 100 items per new/stale. This
+ * will be fixed when query pagination is implemented.
+ *
+ * @param {any} client Apollo GraphQL client to use to query the GitHub GraphQL
+ * API. Must be preloaded with the proper credentials.
+ * @param {string[]} options.scanRepos The repositories to scan, in "org/name"
+ * format.
+ * @param {string[]} options.companyUsers Login names of GitHub accounts
+ * associated with employees. This value is used to determine which items
+ * have received a response from someone in the company. Login names should be
+ * usernames not emails (ex. "prototypicalpro")
+ * @param {string[]} options.ignoreLabels Issue/PR labels to exclude (ex."bug").
+ * All issues/PRs with these labels will be excluded from the results.
+ * @param {number} options.staleTime Duration in milliseconds that a item should
+ * remain inactive for it to be considered stale.
+ * @returns {{
+ * newSearchCount: number;
+ * newSearchItems: object[];
+ * staleSearchCount: number;
+ * staleSearchItems: object[];
+ * }}
+ * An object containing the results of the queries. The "items" arrays will contain objects of either
+ * ISSUE_FRAGMENT or PR_FRAGMENT structure.
+ */
+export async function findDashboardItems(client, options) {
+ const { scanRepos, companyUsers, staleTime, ignoreLabels } = options;
+ const staleDate = new Date(Date.now() - staleTime);
+ // fetch all the data
+ const [newRes, staleRes] = await Promise.all([
+ client.query({
+ query: SEARCH_NEW_ITEMS_QUERY,
+ variables: {
+ query: makeNewSearch(companyUsers, scanRepos, ignoreLabels),
+ },
+ }),
+ client.query({
+ query: SEARCH_STALE_ITEMS_QUERY,
+ variables: {
+ queryDefStale: makeDefStaleSearch(
+ companyUsers,
+ scanRepos,
+ ignoreLabels,
+ staleDate
+ ),
+ queryMaybeStale: makeMaybeStaleSearch(
+ companyUsers,
+ scanRepos,
+ ignoreLabels,
+ staleDate
+ ),
+ timeSince: staleDate.toISOString(),
+ },
+ }),
+ ]);
+ // if every comment by an employee is stale, then the issue is stale
+ const filteredMaybeItems = filterMaybeStaleItems(
+ staleRes.data.maybeStale.nodes,
+ companyUsers,
+ staleDate
+ );
+ return {
+ newSearchCount: Math.max(
+ newRes.data.search.issueCount,
+ newRes.data.search.nodes.length
+ ),
+ newSearchItems: newRes.data.search.nodes,
+ staleSearchCount:
+ Math.max(
+ staleRes.data.definitelyStale.issueCount,
+ staleRes.data.definitelyStale.nodes.length
+ ) + filteredMaybeItems.length,
+ staleSearchItems: staleRes.data.definitelyStale.nodes.concat(
+ filteredMaybeItems
+ ),
+ };
+}
+
+/**
+ * Get some useful information about the user we are currently authenticated as.
+ * This function can both test the authentication mechanism and fetch
+ * information about the account that can be used to provide helpful
+ * suggestions in the settings.
+ *
+ * @param {any} client Apollo GraphQL client to use to query the GitHub GraphQL
+ * API.
+ * @param {string} token GitHub personal access token to use for authenticating
+ * this request
+ * @returns {Promise} A promise containing the result of the
+ * GET_CUR_USER_INFO_QUERY. Will thrown an exception if the supplied token
+ * is invalid.
+ */
+export async function getUserInfo(client, token) {
+ const { data } = await client.query({
+ query: GET_CUR_USER_INFO_QUERY,
+ fetchPolicy: 'network-only',
+ context: {
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ return data;
+}
diff --git a/nerdlets/maintainer-dashboard/index.js b/nerdlets/maintainer-dashboard/index.js
index 984d061..fa9749b 100644
--- a/nerdlets/maintainer-dashboard/index.js
+++ b/nerdlets/maintainer-dashboard/index.js
@@ -1,146 +1,8 @@
import React from 'react';
-import {
- Card,
- CardHeader,
- CardBody,
- HeadingText,
- Spinner,
- Grid,
- GridItem,
- Stack,
- StackItem,
- Spacing,
- Table,
- TableHeader,
- TableHeaderCell,
- TableRow,
- TableRowCell,
- Button,
- Tabs,
- TabsItem,
- BillboardChart,
- BlockText,
- Icon,
- Modal,
- TextField,
- Link,
- Select,
- SelectItem,
- UserStorageMutation,
-} from 'nr1';
-import { KNOWN_LABEL_COLORS } from './issueLabel';
-import SettingsUI from './settings';
-import Dashboard from './dashboard';
-import UserSettingsQuery from './storageUtil';
-import { client } from './graphql/ApolloClientInstance';
-import NewRelicUsers from './data/userdata-sample.json';
-
-const RELICS = Object.values(NewRelicUsers)
- .filter((u) => u.user_type === 'relic' || u.user_type === 'contractor')
- .map((u) => u.login)
- .sort();
-
-export default class MaintainerDashboard extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- queryKey: 0,
- settingsHidden: true,
- };
- }
+import MaintainerDashboard from './components/dashboard';
+export default class Root extends React.PureComponent {
render() {
- return (
-
-
-
- {({ loading, token, settings }) => {
- if (loading)
- return ;
- // create apollo client and settings UI
- const gqlClient = client(token);
- // return settings selection front and center if we have no settings
- if (!token || !settings)
- return (
- ({ name, color }))}
- onSubmit={() =>
- this.setState(({ queryKey }) => ({
- settingsHidden: true,
- queryKey: queryKey + 1,
- }))
- }
- client={gqlClient}
- style={{ maxWidth: '60em' }}
- />
- );
- return (
- // else return the dashboard with a settings modal
- <>
-
- this.setState({ settingsHidden: true })}
- onHideEnd={() =>
- this.setState(({ queryKey }) => ({
- queryKey: queryKey + 1,
- }))
- }
- >
- ({ name, color }))}
- onSubmit={() =>
- this.setState(() => ({
- settingsHidden: true,
- }))
- }
- client={gqlClient}
- />
-
-
-
-
-
-
-
-
-
- >
- );
- }}
-
-
-
- );
+ return ;
}
}
diff --git a/nerdlets/maintainer-dashboard/storageUtil.js b/nerdlets/maintainer-dashboard/util/storageUtil.js
similarity index 100%
rename from nerdlets/maintainer-dashboard/storageUtil.js
rename to nerdlets/maintainer-dashboard/util/storageUtil.js