initial commit

This commit is contained in:
wusj 2020-10-11 15:09:04 +08:00
parent f2c2193866
commit a2e37ff2b6
64 changed files with 13753 additions and 87 deletions

84
.gitignore vendored
View File

@ -1,73 +1,23 @@
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# See https://help.github.com/ignore-files/ for more about ignoring files.
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# dependencies
/node_modules
# Generated files
.idea/**/contentModel.xml
# testing
/coverage
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# production
/build
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea/

View File

@ -1,20 +1,2 @@
#### 从命令行创建一个新的仓库
```bash
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://git.trustie.net/Wusj/snowview.git
git push -u origin master
```
#### 从命令行推送已经创建的仓库
```bash
git remote add origin https://git.trustie.net/Wusj/snowview.git
git push -u origin master
```
# SnowView
add a jenkins test

9794
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "snowview",
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/chance": "^0.7.35",
"@types/classnames": "^2.2.4",
"@types/d3": "^4.11.1",
"@types/jquery": "^3.2.16",
"@types/lodash": "^4.14.85",
"@types/prismjs": "^1.6.5",
"@types/react-redux": "^5.0.12",
"@types/react-router-dom": "^4.2.2",
"@types/react-swipeable-views": "^0.12.1",
"chance": "^1.0.12",
"classnames": "^2.2.6",
"d3": "^4.11.0",
"immutable": "^3.8.2",
"jquery": "^3.2.1",
"js-snackbar": "^0.3.1",
"lodash": "^4.17.4",
"material-ui": "1.0.0-beta.21",
"material-ui-icons": "^1.0.0-beta.17",
"material-ui-next-types": "^1.0.0",
"prismjs": "^1.8.4",
"react": "^16.3.2",
"react-dom": "^16.1.1",
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.2",
"react-scripts-ts": "2.8.0",
"react-swipeable-views": "^0.12.13",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"ts-option": "^1.1.4",
"typeface-roboto": "0.0.45",
"typescript-fsa": "^2.5.0",
"typescript-fsa-reducers": "^0.4.4",
"typescript-fsa-redux-thunk": "^2.0.0-beta.6"
},
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject"
},
"devDependencies": {
"@types/jest": "^21.1.6",
"@types/node": "^8.0.51",
"@types/react": "^16.0.22",
"@types/react-dom": "^16.0.3",
"redux-devtools-extension": "^2.13.2"
},
"homepage": "/SnowGraph"
}

3
public/css/main.css Normal file
View File

@ -0,0 +1,3 @@
g.fade {
display: none;
}

1
public/css/normalize.css vendored Normal file
View File

@ -0,0 +1 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*# sourceMappingURL=normalize.min.css.map */

227
public/css/prism.css Normal file
View File

@ -0,0 +1,227 @@
/* http://prismjs.com/download.html?themes=prism-coy&languages=markup+css+clike+javascript+java */
/**
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
* @author Tim Shedor
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
position: relative;
margin: .5em 0;
overflow: visible;
padding: 0;
}
pre[class*="language-"]>code {
position: relative;
border-left: 10px solid #358ccb;
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
background-color: #fdfdfd;
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
background-size: 3em 3em;
background-origin: content-box;
background-attachment: local;
}
code[class*="language"] {
max-height: inherit;
height: 100%;
padding: 0 1em;
display: block;
overflow: auto;
}
/* Margin bottom to accomodate shadow */
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background-color: #fdfdfd;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-bottom: 1em;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
position: relative;
padding: .2em;
border-radius: 0.3em;
color: #c92c2c;
border: 1px solid rgba(0, 0, 0, 0.1);
display: inline;
white-space: normal;
}
pre[class*="language-"]:before,
pre[class*="language-"]:after {
content: '';
z-index: -2;
display: block;
position: absolute;
bottom: 0.75em;
left: 0.18em;
width: 40%;
height: 20%;
max-height: 13em;
box-shadow: 0px 13px 8px #979797;
-webkit-transform: rotate(-2deg);
-moz-transform: rotate(-2deg);
-ms-transform: rotate(-2deg);
-o-transform: rotate(-2deg);
transform: rotate(-2deg);
}
:not(pre) > code[class*="language-"]:after,
pre[class*="language-"]:after {
right: 0.75em;
left: auto;
-webkit-transform: rotate(2deg);
-moz-transform: rotate(2deg);
-ms-transform: rotate(2deg);
-o-transform: rotate(2deg);
transform: rotate(2deg);
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7D8B99;
}
.token.punctuation {
color: #5F6364;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: #c92c2c;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: #2f9c0a;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: #1990b8;
}
.token.regex,
.token.important {
color: #e90;
}
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: .7;
}
@media screen and (max-width: 767px) {
pre[class*="language-"]:before,
pre[class*="language-"]:after {
bottom: 14px;
box-shadow: none;
}
}
/* Plugin styles */
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
color: #e0d7d1;
}
/* Plugin styles: Line Numbers */
pre[class*="language-"].line-numbers {
padding-left: 0;
}
pre[class*="language-"].line-numbers code {
padding-left: 3.8em;
}
pre[class*="language-"].line-numbers .line-numbers-rows {
left: 0;
}
/* Plugin styles: Line Highlight */
pre[class*="language-"][data-line] {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
pre[data-line] code {
position: relative;
padding-left: 4em;
}
pre .line-highlight {
margin-top: 0;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

19
public/index.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="%PUBLIC_URL%/css/normalize.css">
<link rel="stylesheet" href="%PUBLIC_URL%/css/prism.css">
<link rel="stylesheet" href="%PUBLIC_URL%/css/main.css">
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<title>Software Knowledge Graph</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

15
public/manifest.json Normal file
View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

34
src/App.tsx Normal file
View File

@ -0,0 +1,34 @@
import * as React from 'react';
import { Route, Switch } from 'react-router';
import Sidebar from './components/Sidebar/Sidebar';
import appRoutes from './routes/app';
import { withStyles } from 'material-ui';
import appStyle, { AppStyle } from './variables/styles/AppStyle';
import Footer from './components/Footer/Footer';
const image = require('./assets/img/sidebar.jpg');
const logo = require('./assets/img/logo.png');
class App extends React.Component<AppStyle, {}> {
render() {
const {classes} = this.props;
return (
<div>
<Sidebar logo={logo} logoText="" image={image} routes={appRoutes}/>
<div className={classes.mainPanel}>
<div className={classes.content}>
<Switch>
{appRoutes.map((prop, key) =>
<Route exact={prop.exact} path={prop.path} component={prop.component} key={key}/>
)}
</Switch>
</div>
<Footer/>
</div>
</div>
);
}
}
export default withStyles(appStyle)<{}>(App);

9
src/assets.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}

View File

@ -0,0 +1,61 @@
body {
background-color: #EEEEEE;
color: #3C4858;
margin: 0;
font-family: Roboto, Helvetica, Arial, sans-serif;
font-weight: 300;
line-height: 1.5em;
}
h1 {
font-size: 3em;
line-height: 1.15em;
}
h2 {
font-size: 2.4em;
}
h3 {
font-size: 1.825em;
line-height: 1.4em;
margin: 20px 0 10px;
}
h4 {
font-size: 1.3em;
line-height: 1.4em;
}
h5 {
font-size: 1.25em;
line-height: 1.4em;
margin-bottom: 15px;
}
h6 {
font-size: 1em;
text-transform: uppercase;
font-weight: 500;
}
body, h1, h2, h3, h4, h5, h6 {
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 300;
line-height: 1.5em;
}
a {
color: #9c27b0;
text-decoration: none;
}
a:hover, a:focus {
color: #89229b;
text-decoration: none;
}
pre {
margin: 0;
}

BIN
src/assets/img/carousel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/img/sidebar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -0,0 +1,51 @@
import * as React from 'react';
import * as classNames from 'classnames';
import withStyles from 'material-ui/styles/withStyles';
import { Card, CardHeader, CardContent, CardActions } from 'material-ui';
import { RegularCardStyle, default as regularCardStyle } from '../../variables/styles/RegularCardStyle';
interface RegularCardProps {
plainCard?: boolean;
headerColor?: 'orange' | 'green' | 'red' | 'blue' | 'purple';
cardTitle?: {};
cardSubtitle?: {};
footer?: {};
}
class RegularCard extends React.Component<RegularCardProps & RegularCardStyle, {}> {
public static defaultProps: Partial<RegularCardProps> = {
plainCard: false,
headerColor: 'purple'
};
render() {
const {classes, plainCard, headerColor, cardTitle, cardSubtitle, children, footer} = this.props;
const plainCardClasses = classNames({
[' ' + classes.cardPlain]: plainCard
});
const cardPlainHeaderClasses = classNames({
[' ' + classes.cardPlainHeader]: plainCard
});
return (
<Card className={classes.card + plainCardClasses}>
{cardTitle && <CardHeader
classes={{
root: classes.cardHeader + ' ' + classes[headerColor + 'CardHeader'] + cardPlainHeaderClasses,
title: classes.cardTitle,
subheader: classes.cardSubtitle
}}
title={cardTitle}
subheader={cardSubtitle}
/>}
<CardContent>{children}</CardContent>
{footer !== undefined ? (
<CardActions className={classes.cardActions}>{footer}</CardActions>
) : null}
</Card>
);
}
}
export default withStyles(regularCardStyle)<RegularCardProps>(RegularCard);

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import SwipeableViews from 'react-swipeable-views';
interface CarouselProp {
slideCount: number;
}
interface CarouselState {
index: number;
}
class Carousel extends React.Component<CarouselProp, CarouselState> {
timer?: number;
state: CarouselState = {
index: 0
};
startInterval = () => {
if (this.timer) {
window.clearInterval(this.timer);
}
this.timer = window.setInterval(this.handleInterval, 6000);
}
handleInterval = () => {
const { slideCount } = this.props;
this.setState(prevState => ({
index: (prevState.index + 1) % slideCount
}));
}
componentDidMount() {
this.startInterval();
}
componentWillUnmount() {
if (this.timer) {
window.clearInterval(this.timer);
}
}
render() {
const {children} = this.props;
return (
<SwipeableViews index={this.state.index}>
{children}
</SwipeableViews>
);
}
}
export default Carousel;

View File

@ -0,0 +1,57 @@
import * as React from 'react';
import { Dialog, withStyles, WithStyles, Typography } from 'material-ui';
import * as Prism from 'prismjs';
import { Theme } from 'material-ui/styles';
const styles = (theme: Theme) => ({
container: {
margin: theme.spacing.unit * 2,
overflow: 'auto',
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
},
button: {
display: 'inline',
}
}) as React.CSSProperties;
interface CodeModalProps {
content: string;
label: string;
code: boolean;
}
type CodeModalStyle = WithStyles<'container' | 'button'>;
class CodeModal extends React.Component<CodeModalProps & CodeModalStyle, { open: boolean }> {
state = {
open: false
};
handleClickOpen = () => {
this.setState({open: true});
}
handleRequestClose = () => {
this.setState({open: false});
}
render() {
const {classes, code, content, label} = this.props;
const c = code ? Prism.highlight(content, Prism.languages.javascript) : content;
return (
<span>
<Typography component="a" {...{href: '#', onClick: this.handleClickOpen}}>
{label}
</Typography>
<Dialog fullWidth={true} maxWidth="md" onRequestClose={this.handleRequestClose} open={this.state.open}>
<pre className={classes.container} dangerouslySetInnerHTML={{__html: c}}/>
</Dialog>
</span>
);
}
}
export default withStyles(styles)<CodeModalProps>(CodeModal);

148
src/components/D3Chord.tsx Normal file
View File

@ -0,0 +1,148 @@
import * as React from 'react';
import * as d3 from 'd3';
import { ChordGroup, ChordSubgroup, Chord, color, rgb } from 'd3';
import { Chords } from 'd3-chord';
interface D3GraphProps {
id: string;
data: number[][];
colors: string[];
labels: string[];
}
function mix(color1: string, color2: string) {
const c1 = color(color1);
const c2 = color(color2);
const newColor = rgb(
(c1.rgb().r + c2.rgb().r) / 2,
(c1.rgb().g + c2.rgb().g) / 2,
(c1.rgb().b + c2.rgb().b) / 2
);
return newColor.toString();
}
class D3Chord extends React.Component<D3GraphProps, {}> {
canvas: d3.Selection<SVGGElement, Chords, HTMLElement, {}>;
ribbons: d3.Selection<SVGGElement, Chord, SVGGElement, {}>;
update = () => {
const outerRadius = 30, innerRadius = outerRadius - 2;
const {data, colors, labels} = this.props;
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const arc = d3.arc<ChordGroup>()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon<Chord, ChordSubgroup>()
.radius(innerRadius);
const svg = d3.select<SVGSVGElement, {}>(`#${this.props.id}`);
const width = 100;
const height = 100;
const binaryData: number[][] = [];
for (let i = 0; i < data.length; i++) {
binaryData.push([]);
for (let j = 0; j < data[i].length; j++) {
binaryData[i].push(Math.log(data[i][j] + 1));
}
}
this.canvas = svg
.append<SVGGElement>('g')
.attr('transform', `translate(${width / 2}, ${height / 2})`)
.datum(chord(binaryData))
.on('onMouseLeave', this.onMouseLeave);
this.ribbons = this.canvas.append<SVGGElement>('g')
.attr('class', 'ribbons')
.selectAll('path')
.data(chords => chords)
.enter()
.append<SVGGElement>('g');
const group = this.canvas.append<SVGGElement>('g')
.attr('class', 'groups')
.selectAll('g')
.data<ChordGroup>(chords => chords.groups)
.enter().append<SVGGElement>('g')
.on('onMouseOver', this.onMouseOver);
group.append('path')
.attr('id', d => `p${d.index}`)
.style('fill', d => colors[d.index])
.attr('d', arc);
group.append('title').html(d => labels[d.index]);
this.ribbons
.append('path')
.attr('d', ribbon)
.style('fill', d => mix(colors[d.source.index], colors[d.target.index]))
.style('stroke-width', '0.1')
.style('stroke', 'black');
this.ribbons
.append('title')
.html(d => data[d.source.index][d.target.index].toString());
group.append('svg:text')
.style('font-size', '4px')
.style('text-anchor', 'middle')
.attr('dx', function (d: ChordGroup) {
let anchor = (d.startAngle + d.endAngle) / 2;
let radius = outerRadius + 10;
return radius * Math.sin(anchor);
})
.attr('dy', function (d: ChordGroup) {
let anchor = (d.startAngle + d.endAngle) / 2;
let radius = outerRadius + 10;
return -radius * Math.cos(anchor);
})
.text(function (d: ChordGroup) {
return labels[d.index].substr(0, labels[d.index].indexOf('('));
});
}
onMouseOver = (d: {}, i: {}) => {
this.ribbons.classed('fade', p => {
return p.source.index !== i && p.target.index !== i;
});
}
onMouseLeave = () => {
this.ribbons.classed('fade', false);
}
componentDidMount() {
this.update();
}
componentDidUpdate() {
this.update();
}
render() {
return (
<div style={{width: '100%'}}>
<svg
width="100%"
height={600}
viewBox="0,0,100,100"
id={this.props.id}
/>
</div>
);
}
}
export default D3Chord;

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { withStyles } from 'material-ui';
import footerStyle, { FooterStyle } from '../../variables/styles/FooterStyle';
class Footer extends React.Component<FooterStyle, {}> {
render() {
const {classes} = this.props;
return (
<footer className={classes.footer}>
<div className={classes.container}>
<p className={classes.right}>
<span>
Copyright 2018, Software Engineering Institute, Peking University. All right reserved.
</span>
</p>
</div>
</footer>
);
}
}
export default withStyles(footerStyle)<{}>(Footer);

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import withStyles from 'material-ui/styles/withStyles';
import { Table, TableHead, TableRow, TableCell, TableBody } from 'material-ui';
import matTableStyle, { MatTableStyle } from '../../variables/styles/MatTableStyle';
type CellElement = string | JSX.Element;
type Row = {
key?: string;
columns: CellElement[];
};
interface MatTableProps {
tableHead?: string[];
tableData: Row[];
}
class MatTable extends React.Component<MatTableProps & MatTableStyle, {}> {
render() {
const {classes, tableHead, tableData} = this.props;
return (
<div className={classes.tableResponsive}>
<Table className={classes.table}>
{tableHead !== undefined ? (
<TableHead className={classes.tableHeader}>
<TableRow>
{tableHead.map((prop, key) => (
<TableCell
className={classes.tableCell + ' ' + classes.tableHeadCell}
key={key}
>
{prop}
</TableCell>
))}
</TableRow>
</TableHead>
) : null}
<TableBody>
{tableData.map((row, k1) =>
<TableRow className={classes.tableRow} key={row.key || k1}>
{row.columns.map((prop, k2) => (
<TableCell className={classes.tableCell} key={k2}> {prop} </TableCell>
))}
</TableRow>)}
</TableBody>
</Table>
</div>
);
}
}
export default withStyles(matTableStyle)<MatTableProps>(MatTable);

View File

@ -0,0 +1,69 @@
import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui';
import ExpandMoreIcon from 'material-ui-icons/ExpandMore';
import ExpandLessIcon from 'material-ui-icons/ExpandLess';
import { Theme } from 'material-ui/styles';
import Typography from 'material-ui/Typography';
const styles = (theme: Theme) => ({
detail: {
borderWidth: '0.25px',
borderStyle: 'solid',
borderColor: '#7F7F7F',
padding: '8px',
margin: '8px',
},
cellRank: {
width: '12%'
},
cellMain: {
borderBottom: 'none',
overflowWrap: 'normal',
whiteSpace: 'normal'
},
cellTitle: {
fontSize: 16,
color: '#3f51b5',
fontWeight: 'bold' as 'bold',
display: 'inline',
},
highlight: {
background: theme.palette.primary[50]
}
});
interface RankRowProps {
initExpand: boolean;
title: string;
detail: string;
}
type RankRowStyle = WithStyles<'detail' | 'cellRank' | 'cellMain' | 'cellTitle' | 'highlight'>;
class RankRow extends React.Component<RankRowProps & RankRowStyle, { expand: boolean }> {
state = {
expand: this.props.initExpand
};
handleExpandMore = () => {
this.setState({expand: true});
}
handleExpandLess = () => {
this.setState({expand: false});
}
render() {
const {classes, title, detail} = this.props;
return (
<div className={classes.cellMain}>
<Typography className={classes.cellTitle}>{`${title}`}</Typography>
{!this.state.expand && <ExpandMoreIcon onClick={this.handleExpandMore}/>}
{this.state.expand && <ExpandLessIcon onClick={this.handleExpandLess}/>}
{this.state.expand && <div className={classes.detail} dangerouslySetInnerHTML={{__html: detail}}/>}
</div>
);
}
}
export default withStyles(styles)<RankRowProps>(RankRow);

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Input, withStyles, WithStyles } from 'material-ui';
import { RootState } from '../redux/reducer';
import { Dispatch } from 'redux';
import { Theme } from 'material-ui/styles';
import { ChangeEvent, FormEvent } from 'react';
const styles = (theme: Theme) => ({
container: {
margin: theme.spacing.unit * 2
},
form: {
width: '100%',
},
search: {
marginLeft: theme.spacing.unit * 2,
marginRight: theme.spacing.unit * 2,
width: `calc(100% - ${theme.spacing.unit * 4}px)`,
flex: 1,
},
});
interface SearchFormProps {
query?: string;
predefinedQueries: string[];
callback: Function;
dispatch: Dispatch<RootState>;
}
type SearchFormStyles = WithStyles<'container' | 'form' | 'search'>;
class SearchForm extends React.Component<SearchFormProps & SearchFormStyles, { input: string }> {
state = {
input: ''
};
componentDidMount() {
this.setState({input: this.props.query || ''});
}
componentWillReceiveProps(nextProps: SearchFormProps & SearchFormStyles) {
this.setState({input: nextProps.query || ''});
}
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const {dispatch, callback} = this.props;
dispatch(callback({query: this.state.input}));
}
handleChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({input: event.target.value});
}
render() {
const {classes, predefinedQueries} = this.props;
return (
<form className={classes.form} onSubmit={this.handleSubmit}>
<Input
className={classes.search}
type="search"
placeholder="Ask a question here..."
value={this.state.input}
onChange={this.handleChange}
inputProps={{list: 'predefined-queries'}}
/>
<datalist id="predefined-queries">
{predefinedQueries.map(q => <option key={q} value={q}/>)}
</datalist>
</form>
);
}
}
export default withStyles(styles)<{
predefinedQueries: string[], callback: Function, query?: string
}>(connect()(SearchForm));

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import { Link, NavLink } from 'react-router-dom';
import * as classNames from 'classnames';
import withStyles from 'material-ui/styles/withStyles';
import { Drawer } from 'material-ui';
import { default as List, ListItem, ListItemText, ListItemIcon } from 'material-ui/List';
import { AppRoute } from '../../routes/app';
import sidebarStyle, { SidebarStyle } from '../../variables/styles/SidebarStyle';
interface SidebarProps {
logo: string;
logoText: string;
image: string;
routes: AppRoute[];
}
function activeRoute(routeName: string) {
const index = window.location.pathname.indexOf(routeName);
if (index === -1) { return false; }
if (index + routeName.length === window.location.pathname.length) { return true; }
return window.location.pathname.charAt(index + routeName.length) === '/';
}
class Sidebar extends React.Component<SidebarProps & SidebarStyle, {}> {
render() {
const {classes, logo, logoText, image, routes} = this.props;
const links = (
<List className={classes.list}>
{routes.map((route) => {
const listItemClasses = classNames({
[' ' + classes.blue]: activeRoute(route.path)
});
const whiteFontClasses = classNames({
[' ' + classes.whiteFont]: activeRoute(route.path)
});
return (
<NavLink
to={route.path}
className={classes.item}
activeClassName="active"
key={route.sidebarName}
>
<ListItem button={true} className={classes.itemLink + listItemClasses}>
<ListItemIcon className={classes.itemIcon + whiteFontClasses}>
<route.icon />
</ListItemIcon>
<ListItemText
primary={route.sidebarName}
className={classes.itemText + whiteFontClasses}
disableTypography={true}
/>
</ListItem>
</NavLink>
);
})}
</List>
);
const brand = (
<div className={classes.logo}>
<Link to="/" className={classes.logoLink}>
<div className={classes.logoImage}>
<img src={logo} alt="logo" className={classes.img}/>
</div>
{logoText}
</Link>
</div>
);
return (
<div>
<Drawer type="permanent" open={true} classes={{paper: classes.drawerPaper}}>
{brand}
<div className={classes.sidebarWrapper}>{links}</div>
<div
className={classes.background}
style={{backgroundImage: `url(${image})`}}
/>
</Drawer>
</div>
);
}
}
export default withStyles(sidebarStyle)<SidebarProps>(Sidebar);

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import { Tooltip, withStyles, WithStyles } from 'material-ui';
import Typography from 'material-ui/Typography';
const styles = () => ({
statistic: {
display: 'inline-flex',
flexDirection: 'column' as 'column',
width: '100%',
marginTop: '20px',
marginBottom: '20px',
},
num: {
fontSize: '1.2rem',
fontFamily: 'Lato',
fontWeight: 400 as 400,
textAlign: 'center',
lineHeight: '1em',
color: '#1B1C1D'
},
label: {
fontSize: '0.8em',
fontFamily: 'Lato',
fontWeight: 700 as 700,
whiteSpace: 'nowrap',
overflow: 'hidden' as 'hidden',
textTransform: 'uppercase',
textAlign: 'center',
textOverflow: 'ellipsis',
color: 'rgba(0, 0, 0, 0.87)'
},
});
interface StatisticProps {
num: number;
label: string;
}
type RankRowStyle = WithStyles<'statistic' | 'num' | 'label'>;
class Statistic extends React.Component<StatisticProps & RankRowStyle, {}> {
render() {
const {num, label, classes} = this.props;
return (
<div className={classes.statistic}>
<Typography className={classes.num}>{num}</Typography>
<Tooltip title={label} placement="bottom">
<Typography className={classes.label}>{label}</Typography>
</Tooltip>
</div>
);
}
}
export default withStyles(styles)<StatisticProps>(Statistic);

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import withStyles from 'material-ui/styles/withStyles';
import typographyStyle, { TypographyStyle } from '../../variables/styles/TypographyStyle';
class P extends React.Component<TypographyStyle, {}> {
render() {
const {classes, children} = this.props;
return (
<p className={classes.defaultFontStyle + ' ' + classes.pStyle}>
{children}
</p>
);
}
}
export default withStyles(typographyStyle)<{}>(P);

View File

@ -0,0 +1,278 @@
import * as React from 'react';
import * as d3 from 'd3';
import { ForceLink } from 'd3-force';
import { Option } from 'ts-option';
import D3ForceNode from './D3ForceNode';
import D3ForceLink from './D3ForceLink';
import { INode } from '../../model';
const nodeRadius = 50;
const arrowSize = 12;
interface D3ForceProps<N extends INode, R> {
id: string;
highlight: Option<number>;
nodes: N[];
links: R[];
getNodeColor: (node: N) => string;
getNodeLabel: (node: N) => string;
getNodeText: (node: N) => string;
getLinkID: (link: R) => string;
getLinkText: (link: R) => string;
getSourceNodeID: (link: R) => string;
getTargetNodeID: (link: R) => string;
onNodeClick?: (id: string) => void;
}
interface D3ForceState<N, R> {
nodes: D3Node<N>[];
links: D3Relation<N, R>[];
}
export interface D3Node<N> {
raw: N;
x: number;
y: number;
vx?: number;
vy?: number;
fx?: number | null;
fy?: number | null;
}
interface D3Relation<N, R> {
raw: R;
type: 'single' | 'repeated';
source: D3Node<N>;
target: D3Node<N>;
x1: number;
y1: number;
x2: number;
y2: number;
}
export const D3RelationType = {
SINGLE: 'single' as 'single',
REPEATED: 'repeated' as 'repeated'
};
class D3Force<N extends INode, R> extends React.Component<D3ForceProps<N, R>, D3ForceState<N, R>> {
state = {
nodes: [] as D3Node<N>[],
links: [] as D3Relation<N, R>[]
};
nodes: D3Node<N>[] = [];
links: D3Relation<N, R>[] = [];
canvas: SVGGElement | null;
svg: d3.Selection<SVGGElement, {}, HTMLElement, {}>;
simulation: d3.Simulation<D3Node<N>, D3Relation<N, R>> = d3.forceSimulation<D3Node<N>>()
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide(nodeRadius * 1.5));
updateRelation = (rel: D3Relation<N, R>) => {
const x1 = rel.source.x;
const y1 = rel.source.y;
const x2 = rel.target.x;
const y2 = rel.target.y;
return Object.assign({}, rel, {x1, y1, x2, y2});
}
updateLinks = (nextProps: D3ForceProps<N, R>) => {
const {links, getLinkID, getSourceNodeID, getTargetNodeID} = nextProps;
const newLinks = links.filter(l => !this.links.some(lk => getLinkID(lk.raw) === getLinkID(l)));
this.links = [
...this.links.filter(lk => links.some(l => getLinkID(lk.raw) === getLinkID(l))),
...newLinks.map(l => ({
raw: l,
source: this.nodes.find(n => n.raw.getID() === getSourceNodeID(l))!,
target: this.nodes.find(n => n.raw.getID() === getTargetNodeID(l))!,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
type: D3RelationType.SINGLE
})
)
];
this.links
.filter(l => this.links.some(o => l.source === o.target && l.target === o.source))
.forEach(l => l.type = D3RelationType.REPEATED);
}
updateNodes = (nextProps: D3ForceProps<N, R>) => {
const {nodes} = nextProps;
const newNodes = nodes.filter(n => !this.nodes.some(nd => nd.raw.getID() === n.getID()));
this.nodes = [
...this.nodes.filter((nd: D3Node<N>) => nodes.some(n => nd.raw.getID() === n.getID())),
...newNodes.map(n => ({raw: n, x: 0, y: 0}))
];
}
dragCallback = (node: D3ForceNode, x: number, y: number) => {
const newNode = [...this.state.nodes];
const nd = newNode.find(n => n.raw.getID() === node.props.id);
nd!.fx = x;
nd!.fy = y;
this.setState({
nodes: newNode
});
}
componentDidMount() {
this.simulation.force('link', d3.forceLink().id((d: D3Node<N>) => d.raw.getID()));
this.updateNodes(this.props);
this.updateLinks(this.props);
this.simulation.nodes(this.nodes);
this.simulation.on('tick', () => {
if (this.canvas) {
this.links = this.links.map(this.updateRelation);
this.setState({
nodes: this.simulation.nodes(),
links: this.links
});
}
});
this.svg = d3.select<SVGSVGElement, {}>(`#${this.props.id}`)
.style('width', '100%')
.style('height', '800px')
.call(d3.zoom().on('zoom', () => {
let scale = d3.event.transform.k;
const {x, y} = d3.event.transform;
this.svg.attr('transform', `translate(${x}, ${y}) scale(${scale})`);
}))
.on('dblclick.zoom', null)
.select<SVGGElement>('g')
.attr('width', '100%')
.attr('height', '100%');
this.simulation.force<ForceLink<D3Node<N>, D3Relation<N, R>>>('link')!
.links(this.links)
.strength(0.03);
}
componentWillReceiveProps(nextProps: Readonly<D3ForceProps<N, R>>) {
this.updateNodes(nextProps);
this.updateLinks(nextProps);
this.simulation.nodes(this.nodes);
this.simulation.force<ForceLink<D3Node<N>, D3Relation<N, R>>>('link')!
.links(this.links)
.strength(0.03);
this.simulation.alpha(1).restart();
}
render() {
const {getNodeColor, getNodeLabel, getNodeText, getLinkID, getLinkText, onNodeClick, highlight} = this.props;
const sum = nodeRadius + arrowSize;
const half = sum / 2;
const up = half - arrowSize / 2;
const down = half + arrowSize / 2;
return (
<div style={{width: '100%'}}>
<svg viewBox="-400,-400,800,800" id={this.props.id}>
<defs>
<marker
id="start-arrow"
markerWidth={sum}
markerHeight={sum}
refX={0}
refY={half}
orient="auto"
markerUnits="userSpaceOnUse"
>
<path
d={`M${sum},${up} L${sum},${down} L${nodeRadius},${down} L${nodeRadius},${up} z`}
fill="#FFFFFF"
/>
<path
d={`M${sum},${up} L${sum},${down} L${nodeRadius},${half} z`}
fill="#000000"
/>
</marker>
<marker
id="end-arrow"
markerWidth={sum}
markerHeight={sum}
refX={sum}
refY={half}
orient="auto"
markerUnits="userSpaceOnUse"
>
<path
d={`M0,${up} L0,${down} L${arrowSize},${down} L${arrowSize},${up} z`}
fill="#FFFFFF"
/>
<path
d={`M0,${up} L0,${down} L${arrowSize},${half} z`}
fill="#000000"
/>
</marker>
</defs>
<g ref={g => this.canvas = g}>
<g className="links">
{this.state.links.map(l => {
return (
<D3ForceLink
key={getLinkID(l.raw)}
nodeRadius={nodeRadius}
id={getLinkID(l.raw)}
text={getLinkText(l.raw)}
x1={l.x1}
x2={l.x2}
y1={l.y1}
y2={l.y2}
toSelf={l.source === l.target}
type={l.type}
/>
);
})}
</g>
<g className="nodes">
{this.state.nodes.map(n => (
<D3ForceNode
x={n.x}
y={n.y}
key={n.raw.getID()}
nodeRadius={nodeRadius}
id={n.raw.getID()}
color={getNodeColor(n.raw)}
label={getNodeLabel(n.raw)}
text={getNodeText(n.raw)}
simulation={this.simulation}
dragCallback={this.dragCallback}
onNodeClick={onNodeClick}
highlight={highlight.exists(x => x.toString() === n.raw.getID())}
/>
))}
</g>
</g>
</svg>
</div>
);
}
}
export default D3Force;

View File

@ -0,0 +1,73 @@
import * as React from 'react';
interface D3ForceLinkProps {
id: string;
text: string;
x1: number;
y1: number;
x2: number;
y2: number;
nodeRadius: number;
toSelf: boolean;
type: 'single' | 'repeated';
}
class D3ForceLink extends React.Component<D3ForceLinkProps, {}> {
g: SVGGElement | null;
render() {
const {id, text, x1, y1, x2, y2, type, toSelf, nodeRadius} = this.props;
let d = '';
if (type === 'single') {
d = x1 > x2 ? `M${x2},${y2} L${x1},${y1}` : `M${x1},${y1} L${x2},${y2}`;
} else if (toSelf) {
d = `M${x1 - nodeRadius},${y1}
A${nodeRadius},${nodeRadius} 0 1,0
${x1},${y1 + nodeRadius}`;
} else {
const dx = x2 - x1;
const dy = y2 - y1;
const dxx = dx * dx;
const dyy = dy * dy;
const dxy = dx * dy;
const sx = x1 + x2;
const sy = y1 + y2;
const dis = Math.sqrt(dxx + dyy);
const fx =
(-dxy * y1 + dyy * x1 + 0.5 * dxx * sx + 0.5 * dxy * sy - dis / 5 * dis * dy) / (dxx + dyy);
const fy =
(dxx * y1 - dxy * x1 + 0.5 * dxy * sx + 0.5 * dyy * sy + dis / 5 * dis * dx) / (dxx + dyy);
d = x1 > x2 ?
`M${x2},${y2} Q${fx},${fy} ${x1},${y1}` : `M${x1},${y1} Q${fx},${fy} ${x2},${y2}`;
}
const markerStart = x1 > x2 ? 'url(#start-arrow)' : '';
const markerEnd = x2 > x1 ? 'url(#end-arrow)' : '';
return (
<g ref={x => this.g = x} className="link">
<path
id={`p${id}`}
className="link"
fill="none"
stroke="black"
strokeWidth={3}
d={d}
markerStart={markerStart}
markerEnd={markerEnd}
/>
<text transform="translate(0,-2)" className="link-label" textAnchor="middle">
<textPath xlinkHref={`#p${id}`} startOffset="50%">
{text}
</textPath>
</text>
</g>
);
}
}
export default D3ForceLink;

View File

@ -0,0 +1,67 @@
import * as React from 'react';
import * as d3 from 'd3';
interface D3ForceNodeProps {
nodeRadius: number;
id: string;
color: string;
label: string;
text: string;
x: number;
y: number;
simulation: d3.Simulation<{}, undefined>;
dragCallback: (node: D3ForceNode, x: number, y: number) => void;
highlight?: boolean;
onNodeClick?: (id: string) => void;
}
class D3ForceNode extends React.Component<D3ForceNodeProps, {}> {
g: SVGGElement | null;
componentDidMount() {
const {simulation, dragCallback, onNodeClick, id} = this.props;
const node = d3.select(this.g)
.call(d3.drag<SVGGElement, {}>()
.on('start', () => {
if (!d3.event.active) {
simulation.alphaTarget(0.3).restart();
}
})
.on('drag', () => {
dragCallback(this, d3.event.x, d3.event.y);
})
.on('end', () => {
if (!d3.event.active) {
simulation.alphaTarget(0);
}
})
);
if (onNodeClick) {
node.on('click', () => onNodeClick(id));
}
}
render() {
const {nodeRadius, color, label, text, x, y, highlight} = this.props;
return (
<g ref={i => this.g = i} className="node" transform={`translate(${x}, ${y})`}>
<circle
r={nodeRadius}
cx={0}
cy={0}
style={{fill: color}}
strokeWidth={highlight ? 3 : 0}
stroke="black"
/>
<text textAnchor="middle" x={0} y={-5} fontWeight="bold">{label}</text>
<text textAnchor="middle" x={0} y={15}>{text}</text>
</g>
);
}
}
export default D3ForceNode;

21
src/config.ts Normal file
View File

@ -0,0 +1,21 @@
export const SERVER_URL = 'http://106.75.143.22:8004';
export const PROJECTS_INFO_URL = `${SERVER_URL}/projects`;
export const DOCUMENT_SEARCH_URL = `${SERVER_URL}/docSearch`;
export const CODE_SEARCH_URL = `${SERVER_URL}/codeSearch`;
export const NODE_INFO_URL = `${SERVER_URL}/node`;
export const RELATION_LIST_URL = `${SERVER_URL}/relationList`;
export const NAV_URL = `${SERVER_URL}/nav`;
export const DOC_PREDEFINED_QUERIES = [
'How to write a document into an index?',
'How to get all field names in an IndexReader?',
'How to parse wildcard query using TokenFilter?',
];
export const GRAPH_PREDEFINED_QUERIES = [
'Who change class named IndexReader',
'How to write a document into an index?',
'How to get all field names in an IndexReader?',
'How to parse wildcard query using TokenFilter?',
'list method of Class IndexWriter'
];

26
src/index.tsx Normal file
View File

@ -0,0 +1,26 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { appReducer } from './redux/reducer';
import { composeWithDevTools } from 'redux-devtools-extension';
import 'typeface-roboto';
import { BrowserRouter } from 'react-router-dom';
require('../node_modules/js-snackbar/dist/snackbar.css');
require('./assets/css/snowgraph.css');
const store = createStore(appReducer, composeWithDevTools(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
registerServiceWorker();

12
src/library/js-snackbar.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare module 'js-snackbar' {
type ShowOption = {
text: string;
pos?: 'bottom-left' | 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right';
duration: number;
}
export function show(param: ShowOption): void
}

83
src/model.ts Normal file
View File

@ -0,0 +1,83 @@
export interface CypherQueryResult {
cypher?: string;
nodes: Neo4jNode[];
relationships: Neo4jRelation[];
}
export interface NavResult {
nodes: NavNode[];
relationships: NavRelation[];
}
export interface NavRelation {
startNode: number;
endNode: number;
id: number;
count: number;
type: string;
}
export interface NavNode {
id: number;
label: string;
count: number;
}
export interface Neo4jRelation {
startNode: number;
endNode: number;
id: number;
type: string;
}
export interface Neo4jNode {
id: number;
label: string;
properties: { [key: string]: any; };
}
export interface SnowRelation {
shown: boolean;
id: string;
source: number;
target: number;
types: string[];
}
export class SnowNode implements INode {
shown: boolean;
node: Neo4jNode;
constructor(shown: boolean, node: Neo4jNode) {
this.shown = shown;
this.node = node;
}
getID(): string {
return this.node.id.toString();
}
}
export interface INode {
getID(): string;
}
export interface DocumentProperty {
content: string;
html: string;
id: number;
label: string;
title: string;
}
export interface DocumentResult {
rank: number;
id: number;
label: string;
properties: DocumentProperty;
}
export interface ProjectInfo {
name: string;
description: string;
}

97
src/pages/AboutPage.tsx Normal file
View File

@ -0,0 +1,97 @@
import * as React from 'react';
import RegularCard from '../components/Cards/RegularCard';
import MatTable from '../components/MatTable/MatTable';
import { withStyles, WithStyles } from 'material-ui';
import { container } from '../variables/styles';
import { StyleRules } from 'material-ui/styles';
type AboutPageStyleStyleKeys = 'container';
type AboutPageStyle = WithStyles<AboutPageStyleStyleKeys>;
const styles = () => ({
container: {
paddingTop: 40,
...container
}
}) as StyleRules<AboutPageStyleStyleKeys>;
const faculty = [{
key: 'Bing Xie',
columns: ['Bing Xie', 'Professor, Peking University'],
}, {
key: 'Yanzhen Zou',
columns: ['Yanzhen Zou', 'Associated Professor, Peking University'],
}, {
key: 'Junfeng Zhao',
columns: ['Junfeng Zhao', 'Associated Professor, Peking University'],
}];
const projectManager = [{
key: 'Zeqi Lin',
columns: ['Zeqi Lin', 'PhD Student, Peking University'],
}, {
key: 'Min Wang',
columns: ['Min Wang', 'PhD Student, Peking University'],
}];
const committers = [{
key: 'Jinan Ni',
columns: ['Jinan Ni', 'Master Student, Peking University'],
}, {
key: 'Yingkui Cao',
columns: ['Yingkui Cao', 'PhD Student, Peking University'],
}, {
key: 'Chenyan Hua',
columns: ['Chenyan Hua', 'Master Student, Peking University'],
}, {
key: 'Qi Shen',
columns: ['Qi Shen', 'PhD Student, Peking University'],
}, {
key: 'Chunyang Ling',
columns: ['Chunyang Ling', 'Master Student, Peking University'],
}, {
key: 'Wenpeng Li',
columns: ['Wenpeng Li', 'Master Student, Peking University'],
}, {
key: 'Ying Qian',
columns: ['Ying Qian', 'Master Student, Peking University'],
}];
const sponsors = [
{
columns: [`This project is supported by the National Key Research and Development Program of China\
(Grant No. 2016YFB1000801).`]
}, {
columns: [`The computing resource is supported by the National Key Research and Development Program of China\
(Grant No. 2016YFB1000805).`],
}
];
class AboutPage extends React.Component<AboutPageStyle, {}> {
render() {
const {classes} = this.props;
return (
<div className={classes.container}>
<RegularCard cardTitle="Authors" headerColor="blue">
<p>
SnowGraph由北京大学信息科学技术学院软件复用小组研发
SnowGraph的主要研发人员包括2016
</p>
<h5>Faculty:</h5>
<MatTable tableData={faculty}/>
<h5>Project Manager:</h5>
<MatTable tableData={projectManager}/>
<h5>Committers:</h5>
<MatTable tableData={committers}/>
<h5>Sponsors:</h5>
<MatTable tableData={sponsors}/>
</RegularCard>
</div>
);
}
}
export default withStyles(styles)<{}>(AboutPage);

View File

@ -0,0 +1,85 @@
import * as React from 'react';
import logo from '../../assets/img/logo.png';
import { TextField, WithStyles } from 'material-ui';
import { withStyles } from 'material-ui/styles';
import { StyleRules } from 'material-ui/styles/withStyles';
import { ChangeEvent } from 'react';
import { ProjectInfo } from '../../model';
import { PROJECTS_INFO_URL } from '../../config';
import { Link } from 'react-router-dom';
import MatTable from '../../components/MatTable/MatTable';
import { container, flexContainer } from '../../variables/styles';
import RegularCard from '../../components/Cards/RegularCard';
interface CatalogPageState {
filter: string;
projects: ProjectInfo[];
}
type CatalogPageStyleKeys = 'container' | 'flexContainer' | 'search';
type CatalogPageStyle = WithStyles<CatalogPageStyleKeys>;
const styles = () => ({
container,
flexContainer,
search: {
width: '100%'
},
}) as StyleRules<CatalogPageStyleKeys>;
class CatalogPage extends React.Component<CatalogPageStyle, CatalogPageState> {
constructor(props: CatalogPageStyle) {
super(props);
this.state = {
filter: '',
projects: []
};
}
componentDidMount() {
fetch(PROJECTS_INFO_URL)
.then(response => response.json())
.then(r => {
this.setState({projects: r});
});
}
handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({filter: event.target.value});
}
render() {
const {classes} = this.props;
const {filter, projects} = this.state;
const data = projects.filter(p => p.name.toLowerCase().includes(filter.toLowerCase())).map(p => ({
key: p.name,
columns: [
<Link key={p.name} to={`/demo/${p.name}/diagram`}> {p.name} </Link>,
p.description
]
}));
return (
<div className={classes.container}>
<div className={classes.flexContainer}>
<img src={logo} alt="logo"/>
<RegularCard>
<TextField
id="search"
label="Filter Projects"
type="search"
className={classes.search}
onChange={this.handleFilterChange}
/>
<MatTable tableHead={['项目', '简介']} tableData={data}/>
</RegularCard>
</div>
</div>
);
}
}
export default withStyles(styles)<{}>(CatalogPage);

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import { Route, Switch } from 'react-router-dom';
import CatalogPage from './CatalogPage';
import ProjectPage from './ProjectPage/ProjectPage';
class DemoPage extends React.Component<{}, {}> {
render() {
return (
<div>
<Switch>
<Route exact={true} path="/demo" component={CatalogPage}/>
<Route path="/demo/:project/:tab" component={ProjectPage}/>
</Switch>
</div>
);
}
}
export default DemoPage;

View File

@ -0,0 +1,151 @@
import * as React from 'react';
import * as $ from 'jquery';
import { Component } from 'react';
import { Dispatch } from 'redux';
import { RouteComponentProps, withRouter } from 'react-router';
import { LinearProgress, Typography, withStyles, WithStyles } from 'material-ui';
import { History } from 'history';
import { RootState } from '../../../redux/reducer';
import D3Chord from '../../../components/D3Chord';
import { name2color } from '../../../utils/utils';
import Grid from 'material-ui/Grid';
import Statistic from '../../../components/Statistic';
import { container } from '../../../variables/styles';
import { StyleRules } from 'material-ui/styles';
import { NAV_URL } from '../../../config';
import { NavNode, NavRelation, NavResult } from '../../../model';
import { none, Option, some } from 'ts-option';
interface DiagramTabState {
navGraph: {
fetching: boolean;
nodes: NavNode[];
relations: NavRelation[];
matrix: Option<number[][]>;
};
}
interface DiagramTabRouteProps {
project: string;
}
interface DiagramTabProps extends RouteComponentProps<DiagramTabRouteProps> {
dispatch: Dispatch<RootState>;
history: History;
}
type DiagramTabStyleKeys = 'container' | 'white' | 'progress' | 'info';
type DiagramTabStyles = WithStyles<DiagramTabStyleKeys>;
const styles = () => ({
container: {
paddingTop: '60px',
...container
},
info: {
fontSize: '1.8rem',
margin: '20px',
fontWeight: "bold"
}
}) as StyleRules<DiagramTabStyleKeys>;
class DiagramTab extends Component<DiagramTabProps & DiagramTabStyles, DiagramTabState> {
state: DiagramTabState = {
navGraph: {
fetching: false,
nodes: [],
relations: [],
matrix: none
}
};
changeFetch = (fetching: boolean) => {
this.setState(prevState => ({
navGraph: Object.assign(prevState, {fetching})
}));
}
resultToState = (result: NavResult) => {
const ns = result.nodes;
const d = ns.map(() => ns.map(() => 0));
result.relationships.forEach(r => d[r.startNode][r.endNode] += r.count);
return {
navGraph: {
fetching: false,
nodes: result.nodes,
relations: result.relationships,
matrix: some(d)
}
};
}
componentDidMount() {
this.changeFetch(true);
$.post(NAV_URL, {project: this.props.match.params.project})
.then((result: NavResult) => this.setState(this.resultToState(result)))
.catch(() => this.changeFetch(false));
}
render() {
const {classes} = this.props;
const {navGraph} = this.state;
let navBody = <LinearProgress className={classes.progress}/>;
if (!navGraph.fetching) {
let numberNodes = 0 ;
let numberEdges = 0 ;
navGraph.nodes.map( n=>{ numberNodes+=n.count});
navGraph.relations.map(relation =>{numberEdges+=relation.count})
navBody = navGraph.matrix.isEmpty ?
<Typography className={classes.white}>Failed to load nav graph</Typography> : (
<Grid container={true} spacing={8}>
<Grid item={true} xs={5}>
<D3Chord
id="nav-d3"
data={navGraph.matrix.get}
colors={navGraph.nodes.map(n => name2color(n.label))}
labels={navGraph.nodes.map(n => `${n.label}(${n.count})`)}
/>
</Grid>
<Grid item={true} xs={7}>
<Typography className={classes.info}>Nodes: {numberNodes}</Typography>
<Grid container={true} spacing={8}>
{navGraph.nodes.map(node => (
<Grid style={{textAlign: 'center'}} key={node.label} item={true} lg={3} sm={4}>
<Statistic num={node.count} label={node.label}/>
</Grid>
))}
</Grid>
<Typography className={classes.info}>Edges: {numberEdges}</Typography>
<Grid container={true} spacing={8}>
{navGraph.relations.map(relation => (
<Grid
style={{textAlign: 'center'}}
key={`${relation.startNode}-${relation.endNode}-${relation.type}`}
item={true}
lg={3}
sm={4}
>
<Statistic num={relation.count} label={relation.type}/>
</Grid>
))}
</Grid>
</Grid>
</Grid>
);
}
return (
<div className={classes.container}>
{navBody}
</div>
);
}
}
export default withStyles(styles)<{}>(withRouter(DiagramTab));

View File

@ -0,0 +1,71 @@
import * as React from 'react';
import { LinearProgress, withStyles, WithStyles } from 'material-ui';
import { Theme } from 'material-ui/styles';
import { DocumentResultState, RootState } from '../../../redux/reducer';
import RankRow from '../../../components/RankRow';
import SearchForm from '../../../components/SearchForm';
import { connect } from 'react-redux';
import { fetchDocumentResultWorker } from '../../../redux/action';
import { DOC_PREDEFINED_QUERIES } from '../../../config';
import { RouteComponentProps } from 'react-router';
import MatTable from '../../../components/MatTable/MatTable';
import { container } from '../../../variables/styles';
const styles = (theme: Theme) => ({
container: {
...container,
},
progress: {
flexGrow: 1,
margin: theme.spacing.unit * 4
}
}) as React.CSSProperties;
const mapStateToProps = (state: RootState) => ({
documentResult: state.documentResult
});
interface DocumentTabRouteProps {
project: string;
}
interface DocumentTabProps extends RouteComponentProps<DocumentTabRouteProps> {
documentResult: DocumentResultState;
}
type DocumentTabStyle = WithStyles<'container' | 'progress'>;
class DocumentTab extends React.Component<DocumentTabProps & DocumentTabStyle, {}> {
render() {
const {classes, documentResult} = this.props;
const project = this.props.match.params.project;
return (
<div>
<SearchForm
query={documentResult.query}
predefinedQueries={DOC_PREDEFINED_QUERIES}
callback={(param: { query: string }) => fetchDocumentResultWorker({project, query: param.query})}
/>
<div className={classes.container}>
{documentResult.fetching && <LinearProgress className={classes.progress}/>}
{documentResult.result != null &&
<MatTable
tableHead={['Rank', 'ID', 'Answer']}
tableData={documentResult.result.map(r => ({
columns: [`${r.rank}`, `${r.id}`, <RankRow
key={r.id}
initExpand={r.rank === 1}
title={'[' + r.label + '] ' + (r.properties.title == null ? "" : r.properties.title)}
detail={r.properties.html}
/>]
}))}
/>}
</div>
</div>
);
}
}
export default withStyles(styles)<{}>(connect(mapStateToProps)(DocumentTab));

View File

@ -0,0 +1,119 @@
import * as React from 'react';
import { connect } from 'react-redux';
import {
InputLabel, FormControl, LinearProgress, withStyles,
WithStyles, FormGroup
} from 'material-ui';
import { Theme } from 'material-ui/styles';
import { RootState } from '../../../redux/reducer';
import { Dispatch } from 'redux';
import Select from 'material-ui/Select';
import Input from 'material-ui/Input';
import Button from 'material-ui/Button';
import Typography from 'material-ui/Typography';
import { FormEvent } from 'react';
import { Option } from 'ts-option';
import * as _ from 'lodash';
import { fetchNodeWorker, removeNode, showRelations } from '../../../redux/action';
import { RelationListsState, RelationsState } from '../../../redux/graphReducer';
import { Chance } from 'chance';
import RegularCard from '../../../components/Cards/RegularCard';
const chance = new Chance();
const styles = (theme: Theme) => ({
formControl: {
margin: theme.spacing.unit,
minWidth: 240,
}
});
const mapStateToProps = (state: RootState) => ({
selectedNode: state.graph.selectedNode,
relations: state.graph.relations,
relationLists: state.graph.relationLists,
});
interface FindEntityPanelProps {
selectedNode: Option<number>;
relations: RelationsState;
relationLists: RelationListsState;
dispatch: Dispatch<RootState>;
project: string;
}
class FindEntityPanel extends React.Component<FindEntityPanelProps & WithStyles<'formControl'>, {}> {
input: HTMLInputElement;
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const catalog = this.input.value;
const {dispatch, selectedNode, relationLists, relations, project} = this.props;
const relationList = relationLists.get(selectedNode.get);
const rels = relationList.get
.map(x => relations.get(x))
.filter(x => !x.shown)
.filter(x => x.types.some(t => t === catalog));
const readyToShow = rels.length > 0 ? chance.pickset(rels, 5) : [];
readyToShow.forEach(r => {
const source = r.source, target = r.target;
const otherID = source === selectedNode.get ? target : source;
dispatch(fetchNodeWorker({project, id: otherID}));
});
dispatch(showRelations(readyToShow.map(x => x.id)));
}
render() {
let body = null;
const {dispatch, selectedNode, relationLists, relations} = this.props;
if (selectedNode.isEmpty) {
body = <Typography component="p"> No entity selected. </Typography>;
} else {
const selected = selectedNode.get;
const selectedRelationList = relationLists.get(selected);
if (selectedRelationList.isEmpty) {
body = <LinearProgress/>;
} else {
const relationTypes = _.chain(selectedRelationList.get)
.flatMap(x => relations.get(x).types)
.uniq()
.value();
body = (
<form onSubmit={this.handleSubmit}>
<FormGroup>
<FormControl className={this.props.classes.formControl}>
<InputLabel htmlFor="relation-type">Relation Type</InputLabel>
<Select
native={true}
input={<Input id="relation-type" inputRef={(input) => this.input = input}/>}
>
{relationTypes.map(t => <option key={t} value={t}>{t}</option>)}
</Select>
</FormControl>
</FormGroup>
<Button type="submit">EXPAND</Button>
<Button onClick={() => dispatch(removeNode(selected))}>HIDE</Button>
</form>
);
}
}
return (
<RegularCard headerColor="blue" cardTitle="Surf in the knowledge graph">
{body}
</RegularCard>
);
}
}
export default withStyles(styles)<{ project: string }>(connect(mapStateToProps)(FindEntityPanel));

View File

@ -0,0 +1,83 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { RootState } from '../../../redux/reducer';
import D3Force from '../../../components/d3/D3Force';
import { Option } from 'ts-option';
import { SnowNode, SnowRelation } from '../../../model';
import { NodesState, RelationsState } from '../../../redux/graphReducer';
import { fetchRelationListWorker, selectNode } from '../../../redux/action';
import { name2color } from '../../../utils/utils';
import RegularCard from '../../../components/Cards/RegularCard';
const mapStateToProps = (state: RootState) => ({
nodes: state.graph.nodes,
relations: state.graph.relations,
selectedNode: state.graph.selectedNode
});
interface GraphPanelProps {
nodes: NodesState;
relations: RelationsState;
selectedNode: Option<number>;
dispatch: Dispatch<RootState>;
project: string;
}
class Graph extends D3Force<SnowNode, SnowRelation> {
}
class GraphPanel extends React.Component<GraphPanelProps, {}> {
render() {
const {dispatch, selectedNode, project} = this.props;
const nodes = this.props.nodes
.valueSeq()
.flatMap<number, SnowNode>((x?: Option<SnowNode>) => x!.toArray)
.filter(x => x!.shown)
.toArray();
const links = this.props.relations
.valueSeq()
.filter(x => x!.shown)
.filter(x => nodes.some(n => n.node.id === x!.source) &&
nodes.some(n => n.node.id === x!.target))
.toArray();
return (
<RegularCard headerColor="blue" cardTitle="Knowledge Graph Inference Result">
{this.props.nodes.isEmpty() ?
'' :
<Graph
id="neo4jd3"
highlight={selectedNode}
nodes={nodes}
links={links}
getNodeColor={n => name2color(n.node.label)}
getNodeLabel={n => n.node.label}
getNodeText={n => {
let name = '';
name = n.node.properties['_names'] ? n.node.properties['_names'] : name;
name = n.node.properties['_title'] ? n.node.properties['_title'] : name;
name = n.node.properties['title'] ? n.node.properties['title'] : name;
name = n.node.properties['name'] ? n.node.properties['name'] : name;
name = name.replace(/<(?:.|\s)*?>/g, ' ').trim();
name = name.length > 10 ? name.substr(0, 8) + '...' : name;
return name;
}}
getLinkID={d => d.id}
getLinkText={d => d.types.toString()}
getSourceNodeID={d => d.source.toString()}
getTargetNodeID={d => d.target.toString()}
onNodeClick={id => {
dispatch(fetchRelationListWorker({project, id: parseInt(id, 10)}));
dispatch(selectNode(parseInt(id, 10)));
}}
/>}
</RegularCard>
);
}
}
export default connect(mapStateToProps)(GraphPanel);

View File

@ -0,0 +1,74 @@
import * as React from 'react';
import GraphPanel from './GraphPanel';
import FindEntityPanel from './FindEntityPanel';
import InformationPanel from './InformationPanel';
import { Grid, LinearProgress, withStyles, WithStyles } from 'material-ui';
import { connect } from 'react-redux';
import { Theme } from 'material-ui/styles';
import { RootState } from '../../../redux/reducer';
import { Dispatch } from 'redux';
import RegularCard from '../../../components/Cards/RegularCard';
const styles = (theme: Theme) => ({
leftPanel: {
padding: theme.spacing.unit * 2,
},
rightPanel: {
paddingTop: theme.spacing.unit * 2,
paddingRight: theme.spacing.unit * 2,
},
informationPanel: {
marginTop: theme.spacing.unit * 2,
}
});
const mapStateToProps = (state: RootState) => {
return {
fetchingGraph: state.graph.fetching,
cypher: state.graph.cypher,
};
};
interface GraphPanelsProps {
fetchingGraph: boolean;
cypher: string;
project: string;
dispatch: Dispatch<RootState>;
}
type GraphPanelsStyles = WithStyles<'leftPanel' | 'rightPanel' | 'informationPanel'>;
class GraphPanels extends React.Component<GraphPanelsProps & GraphPanelsStyles, {}> {
render() {
const {classes, fetchingGraph, project, cypher} = this.props;
const show = (
<Grid container={true} spacing={0}>
<Grid item={true} xs={12} className={classes.leftPanel}>
{cypher.length > 0 &&
<RegularCard headerColor="blue" cardTitle="Cypher">
<h5>This natural language is translated as the SQL:</h5>
<code>{cypher}</code>
</RegularCard>
}
</Grid>
<Grid item={true} xs={8} className={classes.leftPanel}>
<GraphPanel project={project}/>
</Grid>
<Grid item={true} xs={4} className={classes.rightPanel}>
<FindEntityPanel project={project}/>
<div className={classes.informationPanel}>
<InformationPanel/>
</div>
</Grid>
</Grid>
);
const notShow = (
<Grid container={true} spacing={0}>
<LinearProgress/>
</Grid>
);
return fetchingGraph ? notShow : show;
}
}
export default withStyles(styles)<{ project: string }>(connect(mapStateToProps)(GraphPanels));

View File

@ -0,0 +1,60 @@
import * as React from 'react';
import { connect } from 'react-redux';
import SearchForm from '../../../components/SearchForm';
import GraphPanels from './GraphPanels';
import { fetchGraphWorker } from '../../../redux/action';
import { GRAPH_PREDEFINED_QUERIES } from '../../../config';
import { RootState } from '../../../redux/reducer';
import { RouteComponentProps } from 'react-router';
import { LinearProgress, WithStyles } from 'material-ui';
import { container } from '../../../variables/styles';
import withStyles from 'material-ui/styles/withStyles';
const styles = () => ({
container: {
paddingTop: '60px',
...container
}
});
const mapStateToProps = (state: RootState) => ({
query: state.graph.query,
fetching: state.graph.fetching
});
interface GraphTabRouteProps {
project: string;
}
type GraphTabStyles = WithStyles<'container'>;
interface GraphTabProps extends RouteComponentProps<GraphTabRouteProps> {
query: string;
fetching: boolean;
}
class GraphTab extends React.Component<GraphTabProps & GraphTabStyles, {}> {
render() {
const {project} = this.props.match.params;
const {query, fetching, classes} = this.props;
return (
<div>
<SearchForm
query={query}
predefinedQueries={GRAPH_PREDEFINED_QUERIES}
callback={(param: { query: string }) => fetchGraphWorker({project, query: param.query})}
/>
{fetching ?
<div className={classes.container}>
<LinearProgress/>
</div> :
<GraphPanels project={project}/>
}
</div>
);
}
}
export default withStyles(styles)<{}>(connect(mapStateToProps)(GraphTab));

View File

@ -0,0 +1,100 @@
import * as React from 'react';
import { connect } from 'react-redux';
import {
LinearProgress, Table, TableBody, TableCell, TableRow, Typography, WithStyles
} from 'material-ui';
import withStyles from 'material-ui/styles/withStyles';
import { Option } from 'ts-option';
import * as _ from 'lodash';
import CodeModal from '../../../components/CodeModal';
import { RootState } from '../../../redux/reducer';
import { NodesState } from '../../../redux/graphReducer';
import RegularCard from '../../../components/Cards/RegularCard';
const mapStateToProps = (state: RootState) => {
return {
selectedNode: state.graph.selectedNode,
nodes: state.graph.nodes
};
};
const styles = () => ({
normalCell: {
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}
});
interface InformationPanelProps {
selectedNode: Option<number>;
nodes: NodesState;
}
class InformationPanel extends React.Component<InformationPanelProps & WithStyles<'normalCell'>, {}> {
render() {
let body = null;
const {classes, selectedNode, nodes} = this.props;
if (selectedNode.isEmpty) {
body = <Typography component="p"> No entity selected. </Typography>;
} else {
const selected = nodes.get(selectedNode.get);
if (selected.nonEmpty) {
const node = selected.get.node;
let properties = Object.keys(node.properties)
.map(k => {
let content = node.properties[k];
if (content.length > 80) {
content = (
<CodeModal
code={k === 'content' || k === 'comment'}
label="SHOW"
content={content}
/>
);
} else {
content = <div className={classes.normalCell}>{content.toString()}</div>;
}
return {key: k, label: k, content};
});
properties = _.sortBy(properties, (entry) => {
if (entry.key === 'label') {
return 1;
}
if (entry.key.indexOf('name') !== -1) {
return 2;
}
if (entry.key.indexOf('signature') !== -1) {
return 3;
}
if (entry.key.indexOf('title') !== -1) {
return 4;
}
return 10;
});
body = (
<Table>
<TableBody>
{properties.map(p => <TableRow key={p.key}>
<TableCell>{p.label}</TableCell>
<TableCell>{p.content}</TableCell>
</TableRow>)}
</TableBody>
</Table>
);
} else {
body = <LinearProgress/>;
}
}
return (
<RegularCard headerColor="blue" cardTitle="Read detailed properties">
{body}
</RegularCard>
);
}
}
export default withStyles(styles)<{}>(connect(mapStateToProps)(InformationPanel));

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import QueryTab from './GraphTab';
import Tabs, { Tab } from 'material-ui/Tabs';
import DocumentTab from './DocumentTab';
import DiagramTab from './DiagramTab';
import { Route, RouteComponentProps, Switch } from 'react-router';
interface ProjectPageRouteProps {
project: string;
tab: TabType;
}
type TabType = 'diagram' | 'graph' | 'document';
class ProjectPage extends React.Component<RouteComponentProps<ProjectPageRouteProps>> {
render() {
const {project, tab} = this.props.match.params;
return (
<div>
<Tabs
value={tab}
onChange={(e, v) => this.props.history.push(`/demo/${project}/${v}`)}
indicatorColor="primary"
textColor="primary"
scrollable={true}
scrollButtons="auto"
>
<Tab value="diagram" label="图谱概览"/>
<Tab value="graph" label="智能问答"/>
<Tab value="document" label="语义搜索"/>
</Tabs>
<Switch>
<Route path="/demo/:project/diagram" component={DiagramTab}/>
<Route path="/demo/:project/graph" component={QueryTab}/>
<Route path="/demo/:project/document" component={DocumentTab}/>
</Switch>
</div>
);
}
}
export default ProjectPage;

80
src/pages/HomePage.tsx Normal file
View File

@ -0,0 +1,80 @@
import * as React from 'react';
import logo from '../assets/img/logo.png';
import { WithStyles, Typography } from 'material-ui';
import { withStyles } from 'material-ui/styles';
import { StyleRules } from 'material-ui/styles/withStyles';
import { container, flexContainer, primaryColor } from '../variables/styles';
import Carousel from '../components/Carousel/Carousel';
import { Link } from 'react-router-dom';
type HomePageStyleKeys = 'container' | 'flexContainer' | 'logoTitle' | 'img' | 'swipe';
type HomePageStyle = WithStyles<HomePageStyleKeys>;
const styles = () => ({
container,
flexContainer,
swipe: {
width: '1000px',
},
img: {
maxWidth: 1000,
},
logoTitle: {
position: 'relative',
width: '300px',
left: '18px',
top: '18px'
}
}) as StyleRules<HomePageStyleKeys>;
const emph = (text: string) => (
<span style={{color: primaryColor}}>{text}</span>
);
const carousels = [
require('../assets/img/carousel.png'),
];
class HomePage extends React.Component<HomePageStyle, {}> {
render() {
const {classes} = this.props;
return (
<div className={classes.container}>
<div className={classes.flexContainer}>
<h1 style={{display: 'inline'}}>
What is
<img className={classes.logoTitle} src={logo} alt="SnowGraph"/>
</h1>
<Typography component="h2" type="headline">
SnowGraph ({emph('S')}oftware K{emph('now')}ledge {emph('Graph')})
helps programmers learn how to reuse open source software
projects.
</Typography>
<ul>
<li>
It automatically extracts domain-specific knowledge graphs from the clutter of heterogeneous
software resources (e.g. source code, user tutorial, commit history, mailing lists, issue
reports and forum discussions).
</li>
<li>
It provides intelligent question answering services for programmers to query these software
knowledge graphs.
</li>
</ul>
<h5>
SnowGraph for {<Link to="/demo/Lucene/diagram">Lucene</Link>}
</h5>
<div className={classes.swipe}>
<Carousel slideCount={carousels.length}>
{carousels.map(c => <img className={classes.img} src={c} alt={c} key={c} />)}
</Carousel>
</div>
</div>
</div>
);
}
}
export default withStyles(styles)<{}>(HomePage);

View File

@ -0,0 +1,79 @@
import * as React from 'react';
import { WithStyles, Typography } from 'material-ui';
import { withStyles } from 'material-ui/styles';
import { StyleRules } from 'material-ui/styles/withStyles';
import RegularCard from '../components/Cards/RegularCard';
import { container } from '../variables/styles';
type ResourcePageStyleKeys = 'container';
type ResourcePageStyle = WithStyles<ResourcePageStyleKeys>;
const styles = () => ({
container: {
paddingTop: 40,
...container
}
}) as StyleRules<ResourcePageStyleKeys>;
const githubUrl = 'https://github.com/linzeqipku/intellide-graph';
const bibtex1 = `
@article{lin2017intelligent,
title={Intelligent Development Environment and Software Knowledge Graph},
author={Lin, Ze-Qi and Xie, Bing and Zou, Yan-Zhen and Zhao, Jun-Feng and Li, Xuan-Dong and Wei, Jun and Sun,\
Hai-Long and Yin, Gang},
journal={Journal of Computer Science and Technology},
volume={32},
number={2},
pages={242--249},
year={2017},
publisher={Springer}
}
`;
const bibtex2 = `
@inproceedings{lin2017improving,
title={Improving software text retrieval using conceptual knowledge in source code},
author={Lin, Zeqi and Zou, Yanzhen and Zhao, Junfeng and Xie, Bing},
booktitle={Proceedings of the 32nd IEEE/ACM International Conference on Automated Software Engineering},
pages={123--134},
year={2017},
organization={IEEE Press}
}
`;
class ResourcePage extends React.Component<ResourcePageStyle, {}> {
render() {
const {classes} = this.props;
return (
<div className={classes.container}>
<RegularCard cardTitle="Code" headerColor="red">
<a href={githubUrl}> {githubUrl} </a>
<Typography type="body1">
These programs and documents are distributed without any warranty, express or implied. As the programs were
written for research purposes only, they have not been tested to the degree that would be advisable in any
important application. All use of these programs is entirely at the user's own risk.
</Typography>
</RegularCard>
<RegularCard cardTitle="Documentation" headerColor="green">
<Typography type="body1">
will be uploaded in near future.
</Typography>
</RegularCard>
<RegularCard cardTitle="Papers" headerColor="orange">
<Typography type="body1">
If you use the program in your work, please cite:
</Typography>
<pre style={{wordWrap: 'break-word', whiteSpace: 'pre-wrap'}}>{bibtex1}</pre>
<pre style={{wordWrap: 'break-word', whiteSpace: 'pre-wrap'}}>{bibtex2}</pre>
</RegularCard>
</div>
);
}
}
export default withStyles(styles)<{}>(ResourcePage);

82
src/redux/action.ts Normal file
View File

@ -0,0 +1,82 @@
import * as $ from 'jquery';
import actionCreatorFactory from 'typescript-fsa';
import bindThunkAction from 'typescript-fsa-redux-thunk';
import { CypherQueryResult, DocumentResult, Neo4jRelation } from '../model';
import { Neo4jNode } from '../model';
import { RootState } from './reducer';
import * as _ from 'lodash';
import { CODE_SEARCH_URL, DOCUMENT_SEARCH_URL, NODE_INFO_URL, RELATION_LIST_URL } from '../config';
const actionCreator = actionCreatorFactory();
export const fetchDocumentResult = actionCreator.async<{ project: string, query: string }, DocumentResult[]>(
'FETCH_DOCUMENT_RESULT'
);
export const fetchDocumentResultWorker = bindThunkAction(
fetchDocumentResult,
async (params) => {
return await $.post(DOCUMENT_SEARCH_URL, params);
}
);
export const setCypher = actionCreator<string>('SET_CYPHER');
export const selectNode = actionCreator<number>('SELECT_NODE');
export const removeNode = actionCreator<number>('REMOVE_NODE');
export const addNodes = actionCreator<Neo4jNode[]>('ADD_NODES');
export const fetchNode = actionCreator.async<{ project: string, id: number }, Neo4jNode>('FETCH_NODE');
export const fetchNodeWorker = bindThunkAction(
fetchNode,
async (params, dispatch, getState: () => RootState) => {
const state = getState();
if (state.graph.nodes.has(params.id)) {
const node = state.graph.nodes.get(params.id);
if (node.nonEmpty) {
return node.get.node;
}
}
return await $.post(NODE_INFO_URL, params);
}
);
export const addShownRelations = actionCreator<Neo4jRelation[]>('ADD_SHOWN_RELATIONS');
export const addRelations = actionCreator<Neo4jRelation[]>('ADD_RELATIONS');
export const fetchRelationList = actionCreator.async<{ project: string, id: number }, string[]>('FETCH_RELATION_LIST');
export const fetchRelationListWorker = bindThunkAction(
fetchRelationList,
async (params, dispatch, getState: () => RootState) => {
const state = getState();
if (state.graph.relationLists.has(params.id)) {
const list = state.graph.relationLists.get(params.id);
if (list.nonEmpty) {
return list.get;
}
}
const result: Neo4jRelation[] = await $.post(RELATION_LIST_URL, params);
dispatch(addRelations(result));
return _.uniq(result.map(r => `${r.startNode},${r.endNode}`));
}
);
export const showRelations = actionCreator<string[]>('SHOW_RELATIONS');
export const fetchGraph = actionCreator.async<{ project: string, query: string }, {}>('FETCH_GRAPH');
export const fetchGraphWorker = bindThunkAction(
fetchGraph,
async (params, dispatch) => {
const result: CypherQueryResult = await $.post(CODE_SEARCH_URL, params);
const cypher = result.cypher;
const nodes = result.nodes;
const relations = result.relationships;
if (cypher && cypher.length > 0) {
dispatch(setCypher(cypher));
} else {
dispatch(setCypher(''));
}
dispatch(addNodes(nodes));
dispatch(addShownRelations(relations));
return {};
});

104
src/redux/graphReducer.ts Normal file
View File

@ -0,0 +1,104 @@
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
addNodes, addRelations, addShownRelations,
fetchGraph, fetchNode, fetchRelationList,
removeNode,
selectNode, setCypher, showRelations
} from './action';
import { combineReducers } from 'redux';
import { Neo4jRelation, SnowNode, SnowRelation } from '../model';
import { Map } from 'immutable';
import { none, Option, some } from 'ts-option';
import { withError } from '../utils/utils';
import * as _ from 'lodash';
export type NodesState = Map<number, Option<SnowNode>>;
export type RelationsState = Map<string, SnowRelation>;
export type RelationListsState = Map<number, Option<string[]>>;
export interface GraphState {
fetching: boolean;
selectedNode: Option<number>;
query: string;
cypher: string;
nodes: NodesState;
relations: RelationsState;
relationLists: RelationListsState;
}
function r2id(neo4jRelation: Neo4jRelation) {
return `${neo4jRelation.startNode},${neo4jRelation.endNode}`;
}
const fetching = reducerWithInitialState<boolean>(false)
.case(fetchGraph.started, () => true)
.case(fetchGraph.done, () => false)
.case(fetchGraph.failed, () => withError('Failed to execute CypherQuery', false));
const selectedNode = reducerWithInitialState<Option<number>>(none)
.case(fetchGraph.started, () => none)
.case(selectNode, (s, p) => some(p))
.case(removeNode, (s, p) => s.exists(num => num === p) ? none : s);
const nodes = reducerWithInitialState<NodesState>(Map())
.case(fetchGraph.started, () => Map())
.case(fetchNode.started, (s, p) => s.update(p.id, (val = none) => val))
.case(fetchNode.done, (s, p) => s.set(p.params.id, some(new SnowNode(true, p.result))))
.case(
fetchNode.failed,
(s, p) => withError('Failed to get node', s.get(p.params.id).isEmpty ? s.remove(p.params.id) : s)
)
.case(addNodes, (s, p) => p.reduce((prev, n) => prev.set(n.id, some(new SnowNode(true, n))), s))
.case(removeNode, (s, p) => s.set(p, s.get(p).map(sn => new SnowNode(false, sn.node))));
const relations = reducerWithInitialState<RelationsState>(Map())
.case(fetchGraph.started, () => Map())
.case(addShownRelations, (s, p) => {
const grouped = _.groupBy(p, r2id);
let newState = s;
for (const id of Object.keys(grouped)) {
const [source, target] = id.split(',').map(x => parseInt(x, 10));
const types = _.uniq(grouped[id].map(r => r.type));
newState = newState.set(id, {shown: true, source, target, id, types});
}
return newState;
})
.case(addRelations, (s, p) => {
const grouped = _.groupBy(p, r2id);
let newState = s;
for (const id of Object.keys(grouped)) {
const [source, target] = id.split(',').map(x => parseInt(x, 10));
const types = _.uniq(grouped[id].map(r => r.type));
if (newState.has(id)) {
newState = newState.update(id, r => Object.assign({}, r, {types: _.uniq([...r.types, ...types])}));
} else {
newState = newState.set(id, {shown: false, source, target, id, types});
}
}
return newState;
})
.case(showRelations, (s, p) =>
p.reduce((prev, r) => prev.update(r, old => Object.assign({}, old, {shown: true})), s))
.case(removeNode, (s, p) => (s.map(value => value!.source === p || value!.target === p ?
Object.assign({}, value, {shown: false}) : value!) as Map<string, SnowRelation>
));
const relationLists = reducerWithInitialState<RelationListsState>(Map())
.case(fetchGraph.started, () => Map())
.case(fetchRelationList.started, (s, p) => s.set(p.id, s.get(p.id, none)))
.case(fetchRelationList.done, (s, p) => s.set(p.params.id, some(p.result)))
.case(fetchRelationList.failed, (s, p) =>
withError('Failed to get relation list', s.remove(p.params.id))
);
const query = reducerWithInitialState<string>('')
.case(fetchGraph.started, (s, p) => p.query);
const cypher = reducerWithInitialState<string>('')
.case(setCypher, (state, payload) => payload);
export const graph = combineReducers({
fetching, selectedNode, query, cypher, nodes, relations, relationLists
});

51
src/redux/reducer.ts Normal file
View File

@ -0,0 +1,51 @@
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
fetchDocumentResult
} from './action';
import { combineReducers } from 'redux';
import { DocumentResult } from '../model';
import { show } from 'js-snackbar';
import { graph, GraphState } from './graphReducer';
function showError(message: string) {
show({
text: message,
pos: 'bottom-center',
duration: 1000
});
}
function withError<V>(message: string, value: V): V {
showError(message);
return value;
}
export interface DocumentResultState {
fetching: boolean;
query: string;
result?: DocumentResult[];
}
export interface RootState {
fetchingRandomQuestion: boolean;
graph: GraphState;
documentResult: DocumentResultState;
}
const documentResult =
reducerWithInitialState<DocumentResultState>({fetching: false, query: ''})
.case(fetchDocumentResult.started, (state, payload) => ({fetching: true, query: payload.query}))
.case(fetchDocumentResult.done, (state, payload) => {
for (let i = 0; i < payload.result.length; ++i) {
payload.result[i].rank = i + 1;
}
return {
fetching: false, query: payload.params.query, result: payload.result
};
})
.case(fetchDocumentResult.failed, (state, payload) =>
withError('Failed to rank', {fetching: false, query: payload.params.query}));
export const appReducer = combineReducers({
graph, documentResult
});

View File

@ -0,0 +1,114 @@
// tslint:disable:no-console
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the 'N+1' visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString()
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (!isLocalhost) {
// Is not local host. Just register service worker
registerValidSW(swUrl);
} else {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log('Content is cached for offline use.');
}
}
};
}
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type')!.indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

64
src/routes/app.ts Normal file
View File

@ -0,0 +1,64 @@
import {
Home,
Dashboard,
Code,
Person,
// ContentPaste,
// LibraryBooks,
// BubbleChart,
// LocationOn,
// Notifications
} from 'material-ui-icons';
import { SvgIconProps } from 'material-ui/SvgIcon';
import HomePage from '../pages/HomePage';
import DemoPage from '../pages/DemoPage/DemoPage';
import ResourcePage from '../pages/ResourcePage';
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
import AboutPage from '../pages/AboutPage';
export interface AppRoute {
path: string;
sidebarName: string;
navbarName: string;
icon: React.ComponentType<SvgIconProps>;
exact: boolean;
component: React.ComponentType<RouteComponentProps<{}>> | React.ComponentType<{}>;
}
const appRoutes: AppRoute[] = [
{
path: '/',
sidebarName: 'Intro',
navbarName: 'Intro',
icon: Home,
exact: true,
component: HomePage
},
{
path: '/demo',
sidebarName: 'Use It',
navbarName: 'Use It',
icon: Dashboard,
exact: false,
component: DemoPage
},
{
path: '/resource',
sidebarName: 'Resource',
navbarName: 'Resource',
icon: Code,
exact: true,
component: ResourcePage
},
{
path: '/about',
sidebarName: 'About',
navbarName: 'About',
icon: Person,
exact: true,
component: AboutPage
}
];
export default appRoutes;

5
src/routes/index.ts Normal file
View File

@ -0,0 +1,5 @@
import App from '../App';
const indexRoutes = [{ path: '/', component: App }];
export default indexRoutes;

70
src/utils/utils.ts Normal file
View File

@ -0,0 +1,70 @@
import { show } from 'js-snackbar';
import { hsl } from 'd3-color';
export function showError(message: string) {
show({
text: message,
pos: 'bottom-center',
duration: 1000
});
}
export function withError<V>(message: string, value: V): V {
showError(message);
return value;
}
const I64BIT_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'.split('');
function hash0(input: string): string {
let hash = 5381;
let i = input.length - 1;
for (; i > -1; i--) {
hash += (hash << 5) + input.charCodeAt(i);
}
let value = hash & 0x7FFFFFFF;
let retValue = '';
do {
retValue += I64BIT_TABLE[value & 0x3F];
}
while (value >>= 6);
return retValue;
}
export function colorHash(s: string): number {
s = hash0(s);
let h = 0;
for (let i = 0; i < s.length; ++i) {
const chr = s.charCodeAt(i);
h = h * 79 + chr;
}
return h;
}
export function name2color(name: string): string {
if (name === 'Class') {
return '#43CD80';
}
if (name === 'Method') {
return '#9AFF9A';
}
if (name === 'Field') {
return '#C1FFC1';
}
if (name === 'GitUser') {
return '#D3D3D3';
}
if (name === 'Commit') {
return '#708090';
}
if (name === 'Docx') {
return '#4169E1';
}
const h = colorHash(name) % 360;
return Math.ceil(h / 30) % 2 === 0 ?
hsl(h, 1, 0.8).rgb().toString() :
hsl(h, 1, 0.6).rgb().toString();
}

162
src/variables/styles.ts Normal file
View File

@ -0,0 +1,162 @@
import * as React from 'react';
const drawerWidth = 200;
const footerHeight = 80;
const transition = {
transition: 'all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1)'
};
const container = {
paddingRight: '60px',
paddingLeft: '60px',
};
const flexContainer = {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
};
const content = {
marginTop: '30px',
padding: '30px 15px',
};
const boxShadow = {
boxShadow:
'0 10px 30px -12px rgba(0, 0, 0, 0.42), 0 4px 25px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2)'
};
const card = {
display: 'inline-block',
position: 'relative',
width: '100%',
margin: '25px 0',
boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.14)',
borderRadius: '3px',
color: 'rgba(0, 0, 0, 0.87)',
background: '#fff'
} as React.CSSProperties;
const defaultFont = {
fontFamily: '"Roboto" , "Noto Sans CJK SC", "Helvetica", "Arial", sans-serif',
fontWeight: 300,
lineHeight: '1.5em'
} as React.CSSProperties;
const primaryColor = '#9c27b0';
const warningColor = '#ff9800';
const dangerColor = '#f44336';
const successColor = '#4caf50';
const infoColor = '#00acc1';
const roseColor = '#e91e63';
const grayColor = '#999999';
const primaryBoxShadow = {
boxShadow:
`0 12px 20px -10px rgba(156, 39, 176, 0.28),\
0 4px 20px 0px rgba(0, 0, 0, 0.12),\
0 7px 8px -5px rgba(156, 39, 176, 0.2)`
};
const infoBoxShadow = {
boxShadow:
`0 12px 20px -10px rgba(0, 188, 212, 0.28),\
0 4px 20px 0px rgba(0, 0, 0, 0.12),\
0 7px 8px -5px rgba(0, 188, 212, 0.2)`
};
const successBoxShadow = {
boxShadow:
`0 12px 20px -10px rgba(76, 175, 80, 0.28),\
0 4px 20px 0px rgba(0, 0, 0, 0.12),\
0 7px 8px -5px rgba(76, 175, 80, 0.2)`
};
const warningBoxShadow = {
boxShadow:
`0 12px 20px -10px rgba(255, 152, 0, 0.28),\
0 4px 20px 0px rgba(0, 0, 0, 0.12),\
0 7px 8px -5px rgba(255, 152, 0, 0.2)`
};
const dangerBoxShadow = {
boxShadow:
`0 12px 20px -10px rgba(244, 67, 54, 0.28),\
0 4px 20px 0px rgba(0, 0, 0, 0.12),\
0 7px 8px -5px rgba(244, 67, 54, 0.2)`
};
const orangeCardHeader = {
background: 'linear-gradient(60deg, #ffa726, #fb8c00)',
...warningBoxShadow
};
const greenCardHeader = {
background: 'linear-gradient(60deg, #66bb6a, #43a047)',
...successBoxShadow
};
const redCardHeader = {
background: 'linear-gradient(60deg, #ef5350, #e53935)',
...dangerBoxShadow
};
const blueCardHeader = {
background: 'linear-gradient(60deg, #26c6da, #00acc1)',
...infoBoxShadow
};
const purpleCardHeader = {
background: 'linear-gradient(60deg, #ab47bc, #8e24aa)',
...primaryBoxShadow
};
const cardActions = {
margin: '0 20px 10px',
paddingTop: '10px',
borderTop: '1px solid #eeeeee',
height: 'auto',
...defaultFont
};
const cardHeader = {
margin: '-20px 15px 0',
borderRadius: '3px',
padding: '15px'
};
const defaultBoxShadow = {
border: '0',
borderRadius: '3px',
boxShadow:
'0 10px 20px -12px rgba(0, 0, 0, 0.42), 0 3px 20px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2)',
padding: '10px 0',
transition: 'all 150ms ease 0s'
};
export {
drawerWidth,
footerHeight,
transition,
container,
flexContainer,
content,
boxShadow,
card,
defaultFont,
primaryColor,
warningColor,
dangerColor,
successColor,
infoColor,
roseColor,
grayColor,
primaryBoxShadow,
infoBoxShadow,
successBoxShadow,
warningBoxShadow,
dangerBoxShadow,
orangeCardHeader,
greenCardHeader,
redCardHeader,
blueCardHeader,
purpleCardHeader,
cardActions,
cardHeader,
defaultBoxShadow
};

View File

@ -0,0 +1,24 @@
import { WithStyles } from 'material-ui';
import { drawerWidth, footerHeight, transition } from '../styles';
import { StyleRules } from 'material-ui/styles';
type AppStyleKeys = 'mainPanel' | 'content';
export type AppStyle = WithStyles<AppStyleKeys>;
const appStyle = () => ({
mainPanel: {
width: `calc(100% - ${drawerWidth}px)`,
overflow: 'auto',
position: 'relative',
float: 'right',
...transition,
maxHeight: '100%',
overflowScrolling: 'touch'
},
content: {
minHeight: `calc(100vh - ${footerHeight}px)`
},
}) as StyleRules<AppStyleKeys>;
export default appStyle;

View File

@ -0,0 +1,25 @@
import { defaultFont, container } from '../styles';
import { WithStyles } from 'material-ui';
import { StyleRules } from 'material-ui/styles';
type FooterStyleKeys = 'right' | 'footer' | 'container';
export type FooterStyle = WithStyles<FooterStyleKeys>;
const footerStyle = {
right: {
padding: '15px 0',
margin: '0',
fontSize: '14px',
float: 'right!important'
},
footer: {
bottom: '0',
borderTop: '1px solid #e7e7e7',
padding: '15px 0',
...defaultFont
},
container,
} as StyleRules<FooterStyleKeys>;
export default footerStyle;

View File

@ -0,0 +1,45 @@
import { WithStyles } from 'material-ui';
import { defaultFont, infoColor } from '../styles';
import { StyleRules, Theme } from 'material-ui/styles';
type MatTableStyleKeys = 'tableHeader' | 'table' | 'tableHeadCell' | 'tableRow' | 'tableCell' | 'tableResponsive';
export type MatTableStyle = WithStyles<MatTableStyleKeys>;
const matTableStyle = (theme: Theme) => ({
tableHeader: {
color: infoColor
},
table: {
marginBottom: '0',
width: '100%',
maxWidth: '100%',
backgroundColor: 'transparent',
borderSpacing: '0',
borderCollapse: 'collapse'
},
tableHeadCell: {
color: 'inherit',
...defaultFont,
fontSize: '1.2em',
fontWeight: 400
},
tableRow: {
'&:last-child > td': {
borderBottom: '0'
}
},
tableCell: {
...defaultFont,
lineHeight: '1.42857143',
padding: '12px 8px',
verticalAlign: 'middle'
},
tableResponsive: {
width: '100%',
marginTop: theme.spacing.unit,
overflowX: 'auto'
}
}) as StyleRules<MatTableStyleKeys>;
export default matTableStyle;

View File

@ -0,0 +1,64 @@
import { WithStyles } from 'material-ui';
import {
blueCardHeader, card, cardHeader, defaultFont, greenCardHeader, orangeCardHeader, purpleCardHeader,
redCardHeader
} from '../styles';
import { StyleRules } from 'material-ui/styles';
type RegularCardStyleKeys = 'card' |
'cardPlain' |
'cardHeader' |
'cardPlainHeader' |
'orangeCardHeader' |
'greenCardHeader' |
'redCardHeader' |
'blueCardHeader' |
'purpleCardHeader' |
'cardTitle' |
'cardSubtitle' |
'cardActions';
export type RegularCardStyle = WithStyles<RegularCardStyleKeys>;
const regularCardStyle = () => ({
card,
cardPlain: {
background: 'transparent',
boxShadow: 'none'
},
cardHeader: {
// display: 'inline-block',
// width: '200px',
...cardHeader,
...defaultFont
},
cardPlainHeader: {
marginLeft: 0,
marginRight: 0
},
orangeCardHeader,
greenCardHeader,
redCardHeader,
blueCardHeader,
purpleCardHeader,
cardTitle: {
color: '#FFFFFF',
marginTop: '0',
marginBottom: '5px',
...defaultFont,
fontSize: '1.125em'
},
cardSubtitle: {
...defaultFont,
marginBottom: '0',
color: 'rgba(255, 255, 255, 0.62)',
margin: '0 0 10px'
},
cardActions: {
padding: '14px',
display: 'block',
height: 'auto'
}
}) as StyleRules<RegularCardStyleKeys>;
export default regularCardStyle;

View File

@ -0,0 +1,166 @@
import { WithStyles } from 'material-ui';
import { boxShadow, defaultFont, drawerWidth, infoColor } from '../styles';
import { StyleRules, Theme } from 'material-ui/styles';
type SidebarStyleKeys = 'drawerPaper' |
'logo' |
'logoLink' |
'logoImage' |
'img' |
'background' |
'list' |
'item' |
'itemLink' |
'itemIcon' |
'itemText' |
'whiteFont' |
'blue' |
'sidebarWrapper';
export type SidebarStyle = WithStyles<SidebarStyleKeys>;
const sidebarStyle = (theme: Theme) => ({
drawerPaper: {
border: 'none',
position: 'fixed',
top: '0',
bottom: '0',
left: '0',
zIndex: 1,
...boxShadow,
width: drawerWidth,
height: '100%',
},
logo: {
position: 'relative',
padding: '15px 15px',
zIndex: 4,
'&:after': {
content: '""',
position: 'absolute',
bottom: '0',
height: '1px',
right: '15px',
width: 'calc(100% - 30px)',
backgroundColor: 'rgba(180, 180, 180, 0.3)'
}
},
logoLink: {
...defaultFont,
textTransform: 'uppercase',
padding: '5px 0',
display: 'block',
fontSize: '18px',
textAlign: 'left',
fontWeight: 400,
lineHeight: '30px',
textDecoration: 'none',
backgroundColor: 'transparent',
'&,&:hover': {
color: '#FFFFFF'
}
},
logoImage: {
width: '30px',
display: 'inline-block',
maxHeight: '30px',
marginLeft: '10px',
marginRight: '15px'
},
img: {
width: '140px',
top: '22px',
position: 'absolute',
verticalAlign: 'middle',
border: '0'
},
background: {
position: 'absolute',
zIndex: 1,
height: '100%',
width: '100%',
display: 'block',
top: '0',
left: '0',
backgroundSize: 'cover',
backgroundPosition: 'center center',
'&:after': {
position: 'absolute',
zIndex: 3,
width: '100%',
height: '100%',
content: '""',
display: 'block',
background: '#000',
opacity: '.8'
}
},
list: {
marginTop: '20px',
paddingLeft: '0',
paddingTop: '0',
paddingBottom: '0',
marginBottom: '0',
listStyle: 'none'
},
item: {
position: 'relative',
display: 'block',
textDecoration: 'none',
},
itemLink: {
width: 'auto',
transition: 'all 300ms linear',
margin: '10px 15px 0',
borderRadius: '3px',
position: 'relative',
display: 'block',
padding: '10px 15px',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'rgba(200, 200, 200, 0.2)'
},
...defaultFont
},
itemIcon: {
width: '24px',
height: '30px',
float: 'left',
marginRight: '15px',
textAlign: 'center',
verticalAlign: 'middle',
color: 'rgba(255, 255, 255, 0.8)'
},
itemText: {
...defaultFont,
margin: '0',
lineHeight: '30px',
fontSize: '14px',
color: '#FFFFFF'
},
whiteFont: {
color: '#FFFFFF'
},
blue: {
backgroundColor: infoColor,
boxShadow:
'0 12px 20px -10px rgba(0,188,212,.28), 0 4px 20px 0 rgba(0,0,0,.12), 0 7px 8px -5px rgba(0,188,212,.2)',
'&:hover': {
backgroundColor: infoColor,
boxShadow:
'0 12px 20px -10px rgba(0,188,212,.28), 0 4px 20px 0 rgba(0,0,0,.12), 0 7px 8px -5px rgba(0,188,212,.2)'
}
},
sidebarWrapper: {
position: 'relative',
height: 'calc(100vh - 75px)',
overflow: 'auto',
width: drawerWidth,
zIndex: 4,
overflowScrolling: 'touch'
}
}) as StyleRules<SidebarStyleKeys>;
export default sidebarStyle;

View File

@ -0,0 +1,90 @@
import {
defaultFont,
primaryColor,
infoColor,
successColor,
warningColor,
dangerColor
} from '../styles';
import { WithStyles } from 'material-ui';
import { StyleRules } from 'material-ui/styles';
type TypographyStyleKeys = 'defaultFontStyle' |
'defaultHeaderMargins' |
'pStyle' |
'quote' |
'quoteText' |
'quoteAuthor' |
'mutedText' |
'primaryText' |
'infoText' |
'successText' |
'warningText' |
'dangerText' |
'smallText' |
'aStyle';
export type TypographyStyle = WithStyles<TypographyStyleKeys>;
const typographyStyle = ({
defaultFontStyle: {
...defaultFont,
fontSize: '14px'
},
defaultHeaderMargins: {
marginTop: '20px',
marginBottom: '10px'
},
pStyle: {
margin: '0 0 10px'
},
quote: {
padding: '10px 20px',
margin: '0 0 20px',
fontSize: '17.5px',
borderLeft: '5px solid #eee'
},
quoteText: {
margin: '0 0 10px',
fontStyle: 'italic'
},
quoteAuthor: {
display: 'block',
fontSize: '80%',
lineHeight: '1.42857143',
color: '#777'
},
mutedText: {
color: '#777'
},
primaryText: {
color: primaryColor
},
infoText: {
color: infoColor
},
successText: {
color: successColor
},
warningText: {
color: warningColor
},
dangerText: {
color: dangerColor
},
smallText: {
fontSize: '65%',
fontWeight: 400,
lineHeight: '1',
color: '#777'
},
aStyle: {
textDecoration: 'none',
backgroundColor: 'transparent',
'&,&:hover': {
color: '#FFFFFF'
}
}
}) as StyleRules<TypographyStyleKeys>;
export default typographyStyle;

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"experimentalDecorators": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"pretty": true,
"typeRoots": ["src/library", "node_modules/@types"]
},
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts",
"src/neo4jd3.ts"
]
}

6
tsconfig.test.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs"
}
}

97
tslint.json Normal file
View File

@ -0,0 +1,97 @@
{
"extends": ["tslint-react"],
"rules": {
"align": [
true,
"parameters",
"arguments",
"statements"
],
"ban": false,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": false,
"forin": true,
"indent": [ true, "spaces" ],
"jsdoc-format": true,
"jsx-no-lambda": false,
"jsx-no-multiline-js": false,
"label-position": true,
"max-line-length": [ true, 120 ],
"member-ordering": [
true,
"public-before-private",
"static-before-instance",
"variables-before-functions"
],
"no-any": true,
"no-arg": true,
"no-console": [
true,
"log",
"error",
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-consecutive-blank-lines": true,
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-shadowed-variable": true,
"no-string-literal": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unused-expression": true,
"no-use-before-declare": true,
"one-line": [
true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single", "jsx-double"],
"radix": true,
"semicolon": [true, "always"],
"switch-default": true,
"trailing-comma": [false],
"triple-equals": [ true, "allow-null-check" ],
"typedef": [
true,
"parameter",
"property-declaration"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-module",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
}
}