Skip to content

Commit

Permalink
Meta: GitHub-sourced Mindmap (#1559)
Browse files Browse the repository at this point in the history
## Motivation for the change, related issues

Renders a mindmap of linked projects from GitHub issues to make
connections and dependencies visible. The project grew so much, I
couldn't process the incoming work in my head anymore. This is a v1 of a
tool I hope to use to:

* Quickly figure out is any particular new issue relevant in the short
term
* Communicate why a specific project matters
* Track the progress we're making towards larger goals – they will
become more apparent as this mindmap grows

![CleanShot 2024-07-01 at 21 31
00@2x](https://github.com/WordPress/wordpress-playground/assets/205419/99df610d-9bfc-455f-9115-0aa720907fa7)

## Future work

* CI-generated public HTML page
* A more reliable way to connect two issues 
* A basic UI to connect another node directly from the app
* Server side rendering to easily include highlighted mindmap fragments
in issues descriptions

## Implementation details

* Downloads the list of issues from Playground-related repositories
using GitHub's GraphQL API
* Connects any two issues/prs with `[Type] Mindmap Node` label
* Connects all issues mentioned in issues/prs with label `[Type] Mindmap
Tree`
* Starts at the
[Roadmap](#525)
issue
* Uses D3.js to render an interactive mindmap with clickable links

## Testing Instructions (or ideally a Blueprint)

Run:

```shell
npm run mindmap
```

And go to http://127.0.0.1:5269/

cc @bgrgicak @brandonpayton
  • Loading branch information
adamziel authored Jul 3, 2024
1 parent d7574b7 commit c5f97ce
Show file tree
Hide file tree
Showing 8 changed files with 1,383 additions and 92 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"format:uncommitted": "nx format --fix --parallel --uncommitted",
"lint": "nx run-many --all --target=lint",
"prepublishOnly": "npm run build",
"mindmap": "cd packages/meta/src/mindmap/v2 && php -S 127.0.0.1:5269",
"preview": "nx preview playground-website",
"recompile:php:web": "nx recompile-php:light:all php-wasm-web && nx recompile-php:kitchen-sink:all php-wasm-web ",
"recompile:php:web:light": "nx recompile-php:light:all php-wasm-web ",
Expand Down
229 changes: 229 additions & 0 deletions packages/meta/src/mindmap/v1/fetch-mindmap-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
const shouldRebuild =
new URLSearchParams(window.location.search).get('rebuild') === 'true';

let __moduleGithubToken = localStorage.getItem('GITHUB_TOKEN');
const repos = [
'wordpress/wordpress-playground',
'wordpress/playground-tools',
'wordpress/blueprints',
'wordpress/blueprints-library',
'adamziel/playground-docs-workflow',
'adamziel/site-transfer-protocol',
];

const comparableKey = (str) => str.toLowerCase();

const graphqlQuery = async (query, variables) => {
const headers = {
'Content-Type': 'application/json',
};
if (__moduleGithubToken) {
headers['Authorization'] = `Bearer ${__moduleGithubToken}`;
}
const response = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
return response.json();
};

async function* iterateIssuesPRs(repo, labels = []) {
const query = `
query GetProjects($cursor: String!, $query: String!) {
search(query: $query, first: 100, type: ISSUE, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
... on Issue {
number
title
url
body
state
repository {
nameWithOwner
}
labels(first: 10) {
nodes {
name
}
}
}
... on PullRequest {
number
title
url
body
state
repository {
nameWithOwner
}
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
`;

let cursor = '';
do {
const labelQuery = labels.map((label) => `"${label}"`).join(',');
const variables = {
cursor,
query:
`repo:${repo}` + (labels.length ? ` label:${labelQuery}` : ''),
};

const response = await graphqlQuery(query, variables);
const edges = response.data.search.edges;
for (const edge of edges) {
if (edge.node) {
const item = edge.node;
const key = comparableKey(
`${item.repository.nameWithOwner}#${item.number}`
);
item.key = key;
item.title = item.title.trim().replace(/^Tracking: /, '');
yield edge.node;
}
}
if (!response.data.search.pageInfo.hasNextPage) {
break;
}
cursor = response.data.search.pageInfo.endCursor;
} while (true);
}

const nodesCacheKey = 'nodes_cache';

const fetchData = async () => {
let allNodes = {};
const allNodesArray = [];
for (const repo of repos) {
for await (const item of iterateIssuesPRs(repo)) {
allNodes[item.key] = item;
}
for await (const item of iterateIssuesPRs(repo, [
'[Type] Mindmap Node',
'[Type] Mindmap Tree',
])) {
allNodes[item.key] = item;
}
}
return allNodes;
};

const getConnectedNodes = (issue) => {
const currentRepo = issue.repository.nameWithOwner;
let connections = [];

const regex1 =
/\bhttps:\/\/github.com\/([^\/]+)\/([^\/]+)\/(?:issues|pull)\/(\d+)\b/g;
const regex2 = /\b#(\d+)\b/g;
let match;

while ((match = regex1.exec(issue.body)) !== null) {
connections.push(`${match[1]}/${match[2]}#${match[3]}`);
}

while ((match = regex2.exec(issue.body)) !== null) {
connections.push(`${currentRepo}#${match[1]}`);
}

return connections.map(comparableKey);
};

const buildEdges = ({ allNodes, rootKey, isEdge }) => {
const seen = {};
const allEdges = {};

const preorderTraversal = (currentNode) => {
const relatedIssueKeys = getConnectedNodes(currentNode).filter(
(edgeKey) => isEdge(currentNode.key, edgeKey)
);

for (const relatedKey of relatedIssueKeys) {
if (!allNodes[relatedKey]) continue;
if (seen[relatedKey]) continue;
seen[relatedKey] = true;

if (!allEdges[currentNode.key]) {
allEdges[currentNode.key] = [];
}

allEdges[currentNode.key].push(relatedKey);
preorderTraversal(allNodes[relatedKey]);
}
};

preorderTraversal(allNodes[rootKey]);
return allEdges;
};

const buildTree = (allNodes, allEdges, rootKey) => {
const node = { ...allNodes[rootKey] };
const childrenKeys = allEdges[rootKey] || [];
node.children = childrenKeys.map((childKey) =>
buildTree(allNodes, allEdges, childKey)
);
return node;
};

export const fetchMindmapData = async ({ githubToken } = {}) => {
if (githubToken) {
__moduleGithubToken = githubToken;
}

const allNodes = await fetchData();
const isEdge = (fromKey, toKey) => {
if (mindmapTrees[fromKey]) {
return true;
}
if (mindmapNodes[fromKey] && mindmapNodes[toKey]) {
return true;
}
return false;
};

const mindmapTrees = {};
const mindmapNodes = {};
for (const key in allNodes) {
if (
allNodes[key].labels.nodes.some(
(label) => label.name === '[Type] Mindmap Tree'
)
) {
mindmapTrees[key] = allNodes[key];
mindmapNodes[key] = allNodes[key];
} else if (
allNodes[key].labels.nodes.some(
(label) => label.name === '[Type] Mindmap Node'
)
) {
mindmapNodes[key] = allNodes[key];
}
}

const rootKey = 'wordpress/wordpress-playground#525';
const allEdges = buildEdges({
allNodes,
rootKey,
isEdge,
});
const tree = buildTree(allNodes, allEdges, rootKey);
console.log({
allNodes,
tree,
});

return tree;
};
13 changes: 13 additions & 0 deletions packages/meta/src/mindmap/v1/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mind Map</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="mindmap"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="script.js"></script>
</body>
</html>
Loading

0 comments on commit c5f97ce

Please sign in to comment.