Skip to content

Commit

Permalink
GitGraph and GitNode UI updates (#331)
Browse files Browse the repository at this point in the history
* GitGraph limits graph size to 50 nodes

* Bump electron from 11.1.1 to 11.2.3 (#330)

Bumps [electron](https://github.com/electron/electron) from 11.1.1 to 11.2.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](electron/electron@v11.1.1...v11.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump @typescript-eslint/eslint-plugin from 4.14.1 to 4.14.2 (#324)

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.14.1 to 4.14.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.14.2/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Nelson <nelsonni@oregonstate.edu>

* Bump ts-loader from 8.0.14 to 8.0.15 (#325)

Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.14 to 8.0.15.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](TypeStrong/ts-loader@v8.0.14...v8.0.15)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump @typescript-eslint/parser from 4.14.1 to 4.14.2 (#326)

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.14.1 to 4.14.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.14.2/packages/parser)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump @testing-library/user-event from 12.6.2 to 12.6.3 (#327)

Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.6.2 to 12.6.3.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md)
- [Commits](testing-library/user-event@v12.6.2...v12.6.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump eslint from 7.18.0 to 7.19.0 (#328)

Bumps [eslint](https://github.com/eslint/eslint) from 7.18.0 to 7.19.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](eslint/eslint@v7.18.0...v7.19.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump @testing-library/react from 11.2.3 to 11.2.5 (#329)

Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.3 to 11.2.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md)
- [Commits](testing-library/react-testing-library@v11.2.3...v11.2.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* useGitHistory hook updated to provide git information with scope and branch

* ESLint max-len rule updated to ignore long import lines

* Fixed incorrect branch scoping in useGitHistory hook

* GitNode colors dynamically set from colors palette

* GitGraph centered on graph element updates

* GitNode opacity, border-style, and Tooltip dynamically set via props

* layoutOptimizer separates nodes and reduces GitNode height/width

* GitGraph adds staged future commits to end of branches, adds branch Tooltips

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
nelsonni and dependabot[bot] authored Feb 9, 2021
1 parent 73ccfa0 commit cb423ca
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ module.exports = {
'tabWidth': 2,
'comments': 140,
'ignoreTrailingComments': true,
'ignorePattern': '^ \\* \\|.*\\|' // disable checks for Markdown tables, since cell rows cannot span multiple lines
'ignorePattern': '(^ \\* \\|.*\\|)|(^import .*)', // disable checks for Markdown tables and long import lines
}],
'no-unused-vars': 'off',

Expand Down
117 changes: 89 additions & 28 deletions src/components/GitGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,115 @@
import React, { useEffect, useState } from 'react';
import ReactFlow, { addEdge, ArrowHeadType, Connection, Edge, FlowElement, Node } from 'react-flow-renderer';
import ReactFlow, { addEdge, ArrowHeadType, Connection, Edge, FlowElement, Node, OnLoadFunc, OnLoadParams } from 'react-flow-renderer';

import type { Repository } from '../types';
import { nodeTypes } from './GitNode';
import { useGitHistory } from '../store/hooks/useGitHistory';
import { ReadCommitResult } from 'isomorphic-git';
import { CommitInfo, useGitHistory } from '../store/hooks/useGitHistory';
import { layoutOptimizer } from '../containers/layout';
import { colorSets } from '../containers/colors';
import { currentBranch, getStatus } from '../containers/git';
import { flattenArray } from '../containers/flatten';

const getGitNode = (commit: CommitInfo, branchHead: string | undefined): Node => ({
id: commit.oid,
type: 'gitNode',
data: {
text: '',
tooltip: '',
color: colorSets[5],
border: '',
branch: branchHead && branchHead === commit.oid ? `${commit.scope}/${commit.branch}` : undefined
},
position: { x: 0, y: 0 }
});

const getGitEdge = (commit: CommitInfo): Edge[] => {
return commit.commit.parent.map(parent => {
return {
id: `e${parent.slice(0, 7)}-${commit.oid.slice(0, 7)}`,
source: parent,
target: commit.oid,
arrowHeadType: ArrowHeadType.ArrowClosed
};
});
};

const getGitStaged = async (commit: CommitInfo, repo: Repository): Promise<(Node | Edge)[]> => {
const currentBranchStatus = await getStatus(repo.root);
if (currentBranchStatus && !['ignored', 'unmodified'].includes(currentBranchStatus)) {
return [{
id: `${commit.oid}*`,
type: 'gitNode',
data: {
text: '',
tooltip: '',
color: colorSets[5],
border: 'dashed',
opacity: '0.6',
branch: `${commit.scope}/${commit.branch}*`
},
position: { x: 0, y: 0 }
},
{
id: `e${commit.oid.slice(0, 7)}-${commit.oid.slice(0, 7)}*`,
source: commit.oid,
target: `${commit.oid}*`,
animated: true,
arrowHeadType: ArrowHeadType.ArrowClosed
}
];
} else {
return [];
}
};

export const GitGraph: React.FunctionComponent<{ repo: Repository }> = props => {
const [elements, setElements] = useState<Array<FlowElement>>([]);
const [reactFlowState, setReactFlowState] = useState<OnLoadParams>();
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const { commits, heads, update } = useGitHistory(props.repo);
const onLoad: OnLoadFunc = (rf) => { setReactFlowState(rf) };

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { update() }, [props.repo]);

useEffect(() => {
if (commits.size > 0 && heads.size > 0) {
console.log(`REPO => repo: ${props.repo.name}, commits: ${commits.size}`);
console.log(JSON.stringify([...heads.entries()], null, 2));
if (reactFlowState && elements.length) {
reactFlowState.fitView();
}
}, [commits, heads, props.repo.name]);
}, [elements, reactFlowState]);

useEffect(() => {
const newElements = [...commits.values()].reduce((prev: Array<FlowElement>, curr: ReadCommitResult): Array<FlowElement> => {
const node: Node = {
id: curr.oid,
type: 'gitNode',
data: { text: '', tooltip: `${curr.oid.slice(0, 7)}\n${curr.commit.message}` },
position: { x: 0, y: 0 }
};
const edges: Edge[] = curr.commit.parent.map(parent => {
return {
id: `e${parent.slice(0, 7)}-${curr.oid.slice(0, 7)}`,
source: parent,
target: curr.oid,
arrowHeadType: ArrowHeadType.ArrowClosed
};
});
return [node, ...prev, ...edges];
}, []);
const optimizedNewElements = layoutOptimizer(newElements);
setElements(optimizedNewElements);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [commits, heads]);
const asyncGraphConstruction = async () => {
const currentCommits = [...commits.values()].slice(Math.max(commits.size - 50, 0)) // limited to 50 most recent commits
const newElements = currentCommits.reduce((prev: Array<FlowElement>, curr: CommitInfo): Array<FlowElement> => {
const branchHead = heads.get(`${curr.scope}/${curr.branch}`);
const node: Node = getGitNode(curr, branchHead);
const edges: Edge[] = getGitEdge(curr);
return [node, ...prev, ...edges];
}, []);
const headsHashes = [...heads.values()];
const headCommits = currentCommits.filter(commit => headsHashes.includes(commit.oid));
// TODO: Until we have copied repositories that exist in a cache (probably in within .syn or .git directory), we will need to
// check through all the open cards on the canvas to determine if any of them are associated with a branch and have changes
// compared to the latest version in the branch. The following line has to wait until this is implemented:
//
// const staged = flattenArray(await Promise.all(headCommits.map(headCommit => getGitStaged(headCommit, props.repo))));
const currentBranchName = await currentBranch({ dir: props.repo.root.toString() });
const currentBranchHash = heads.get(`local/${currentBranchName}`);
const staged = flattenArray(await Promise.all(headCommits
.filter(commit => commit.oid === currentBranchHash)
.map(currentBranchCommit => getGitStaged(currentBranchCommit, props.repo))));
const optimizedNewElements = layoutOptimizer([...newElements, ...staged]);
setElements(optimizedNewElements);
}
asyncGraphConstruction();
}, [commits, heads, props.repo]);

return (<ReactFlow
elements={elements}
nodeTypes={nodeTypes}
onConnect={onConnect}
onLoad={onLoad}
onNodeMouseEnter={(_event, node) => console.log(node.id)}
className='git-flow' />);
}
50 changes: 35 additions & 15 deletions src/components/GitNode.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react';
import { Handle, Position } from 'react-flow-renderer';
import { Handle, NodeProps, Position } from 'react-flow-renderer';
import { Theme, Tooltip, withStyles } from '@material-ui/core';
import { ColorSet } from '../containers/colors';

const customNodeStyles = {
height: 10,
width: 10,
const customNodeStyles = (color: ColorSet, border: string, opacity?: string) => ({
borderRadius: '50%',
background: '#9CABB3',
color: '#FFF',
padding: 10,
};
borderStyle: border,
borderWidth: 'thin',
opacity: opacity,
background: color.primary,
color: color.secondary,
padding: 5,
});

const LightTooltip = withStyles((theme: Theme) => ({
tooltip: {
Expand All @@ -21,15 +23,33 @@ const LightTooltip = withStyles((theme: Theme) => ({
},
}))(Tooltip);

export const GitNode: React.FunctionComponent<{ data: { text: string, tooltip: string } }> = ({ data }) => {
type GitNodeProps = NodeProps & {
data: {
text: string,
tooltip: string,
color: ColorSet,
border: string,
opacity?: string,
branch?: string
}
}

export const GitNode: React.FunctionComponent<GitNodeProps> = props => {
return (
<LightTooltip title={data.tooltip} placement='right'>
<div style={customNodeStyles}>
<Handle type='target' position={Position.Top} style={{ borderRadius: '50%' }} />
<div>{data.text}</div>
<Handle type='source' position={Position.Bottom} style={{ borderRadius: '50%' }} />
props.data.branch ?
<LightTooltip title={props.data.branch} placement='right' open={true} arrow={true} >
<div style={customNodeStyles(props.data.color, props.data.border, props.data.opacity)}>
<Handle type='target' position={Position.Top} style={{ visibility: 'hidden' }} />
<div>{props.data.text}</div>
<Handle type='source' position={Position.Bottom} style={{ visibility: 'hidden' }} />
</div>
</LightTooltip>
:
<div style={customNodeStyles(props.data.color, props.data.border, props.data.opacity)}>
<Handle type='target' position={Position.Top} style={{ visibility: 'hidden' }} />
<div>{props.data.text}</div>
<Handle type='source' position={Position.Bottom} style={{ visibility: 'hidden' }} />
</div>
</LightTooltip>
)
}

Expand Down
26 changes: 26 additions & 0 deletions src/containers/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type ColorSet = {
name: string,
primary: string,
secondary: string
}

// colors from http://clrs.cc/
export const colorSets: Array<ColorSet> = [
{ name: 'navy', primary: '#001f3f', secondary: '#80b5ff' },
{ name: 'blue', primary: '#0074d9', secondary: '#b3dbff' },
{ name: 'aqua', primary: '#7fdbff', secondary: '#004966' },
{ name: 'teal', primary: '#39cccc', secondary: '#000000' },
{ name: 'olive', primary: '#3d9970', secondary: '#163728' },
{ name: 'green', primary: '#2ecc40', secondary: '#0e3e14' },
{ name: 'lime', primary: '#01ff70', secondary: '#00662c' },
{ name: 'yellow', primary: '#ffdc00', secondary: '#665800' },
{ name: 'orange', primary: '#ff851b', secondary: '#663000' },
{ name: 'red', primary: '#ff4136', secondary: '#800600' },
{ name: 'maroon', primary: '#85144b', secondary: '#eb7ab1' },
{ name: 'fuchsia', primary: '#f012be', secondary: '#65064f' },
{ name: 'purple', primary: '#b10dc9', secondary: '#efa9f9' },
{ name: 'black', primary: '#111111', secondary: '#dddddd' },
{ name: 'gray', primary: '#aaaaaa', secondary: '#000000' },
{ name: 'silver', primary: '#dddddd', secondary: '#000000' },
{ name: 'white', primary: '#ffffff', secondary: '#444444' }
]
4 changes: 2 additions & 2 deletions src/containers/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { isNode, isEdge } from 'react-flow-renderer';

export const layoutOptimizer = (rfGraph: Array<FlowElement>): Array<FlowElement> => {
const graph = new dagre.graphlib.Graph();
graph.setGraph({});
graph.setGraph({ nodesep: 80 });
graph.setDefaultEdgeLabel(() => { return {}; });

rfGraph.filter(isNode).map(node => graph.setNode(node.id, { width: 10, height: 10 }));
rfGraph.filter(isNode).map(node => graph.setNode(node.id, { width: 5, height: 5 }));
rfGraph.filter(isEdge).map(edge => graph.setEdge(edge.source, edge.target));
dagre.layout(graph);

Expand Down
22 changes: 15 additions & 7 deletions src/store/hooks/useGitHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { ReadCommitResult } from 'isomorphic-git';
import type { Repository } from '../../types';
import { log } from '../../containers/git';

export type CommitInfo = ReadCommitResult & {
branch: string,
scope: 'local' | 'remote'
}

type useGitHistoryHook = {
commits: Map<string, ReadCommitResult>,
commits: Map<string, CommitInfo>,
heads: Map<string, string>,
update: () => Promise<void>
}
Expand All @@ -22,26 +27,29 @@ type useGitHistoryHook = {
* commit hashes to commits and `heads` maps scoped branch names to the SHA-1 hash of the commit pointed to by HEAD on that branch.
*/
export const useGitHistory = (repo: Repository): useGitHistoryHook => {
const [commits, setCommits] = useState(new Map<string, ReadCommitResult>());
const [commits, setCommits] = useState(new Map<string, CommitInfo>());
const [heads, setHeads] = useState(new Map<string, string>());

const update = useCallback(async () => {
const commitsCache = new Map<string, ReadCommitResult>();
const commitsCache = new Map<string, CommitInfo>();
const headsCache = new Map<string, string>();

const processCommits = async (branch: string, scope: 'local' | 'remote'): Promise<void> => {
const branchCommits = await log({ dir: repo.root.toString(), ref: branch });
// append to the caches only if no entries exist for the new commit or branch
branchCommits.map(commit => (!commitsCache.has(commit.oid)) ? commitsCache.set(commit.oid, commit) : null);
if (!headsCache.has(`${scope}/${branch}`)) headsCache.set(`${scope}/${branch}`, branchCommits[0].oid);
branchCommits.map(commit => (!commitsCache.has(commit.oid)) ?
commitsCache.set(commit.oid, { ...commit, branch: branch, scope: scope }) :
null);
const scopedBranch = `${scope}/${branch}`;
if (!headsCache.has(scopedBranch)) headsCache.set(scopedBranch, branchCommits[0].oid);
}

await Promise.all(repo.local.map(async branch => processCommits(branch, 'local')));
await Promise.all(repo.local.map(async branch => processCommits(branch, 'remote')));
await Promise.all(repo.remote.map(async branch => processCommits(branch, 'remote')));
// replace the `commits` and `heads` states every time, since deep comparisons for all commits is computationally expensive
setCommits(commitsCache);
setHeads(headsCache);
}, [repo.local, repo.root]);
}, [repo.local, repo.remote, repo.root]);

return { commits, heads, update };
}

0 comments on commit cb423ca

Please sign in to comment.