diff --git a/app/config/secret/app.json b/app/config/secret/app.json deleted file mode 100644 index f9a3b96..0000000 --- a/app/config/secret/app.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "production": { - "serverURL": "http://localhost:3001/" - } -} diff --git a/app/package-lock.json b/app/package-lock.json index 1171afc..404b505 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -19,14 +19,6 @@ "tslib": "1.7.1" } }, - "@types/chart.js": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.6.2.tgz", - "integrity": "sha512-Hze3pXv7mFkqtrQ7y3xN6Y3EbiUjFuyDqKBeJjQS8rlC5Ox3nH8YYNzfdhQTP74X9lRxH9pbfr05hIM5a+r+pQ==", - "requires": { - "@types/jquery": "3.2.11" - } - }, "@types/csv-parse": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@types/csv-parse/-/csv-parse-1.1.11.tgz", @@ -268,11 +260,6 @@ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.6.0.tgz", "integrity": "sha512-2A0stT6b61DANLErAfSkeQ77N+A3FbR7ardUJUP3xm9f4W8qtG9ispBYDUX42Fl1EbR0rqSV3IWjbB6ew7hXRw==" }, - "@types/jquery": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.2.11.tgz", - "integrity": "sha512-Hu1JquYz58pcLLJU4AWvtvo0Yq5HbEHGGJV/XlTfJtePxbpT1mzfGr6FlH08wTi/ClR2yUb21/P/lgoH+LrRow==" - }, "@types/lodash": { "version": "4.14.73", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.73.tgz", @@ -576,6 +563,12 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1202,32 +1195,6 @@ "supports-color": "2.0.0" } }, - "chart.js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.6.0.tgz", - "integrity": "sha1-MI+aSwv+1aFUwU9d6x2UcNIqvnE=", - "requires": { - "chartjs-color": "2.1.0", - "moment": "2.18.1" - } - }, - "chartjs-color": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.1.0.tgz", - "integrity": "sha1-nDmsgwzNmJlq6AyfEQhv8SyYp1Y=", - "requires": { - "chartjs-color-string": "0.4.0", - "color-convert": "0.5.3" - } - }, - "chartjs-color-string": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.4.0.tgz", - "integrity": "sha1-V3SNRTCuKNjbClSSGCugbf3y9Gg=", - "requires": { - "color-name": "1.1.3" - } - }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -1384,11 +1351,6 @@ } } }, - "color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" - }, "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", @@ -2219,6 +2181,12 @@ "domelementtype": "1.3.0" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, "duplexify": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz", @@ -2244,6 +2212,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "ejs": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.7.tgz", + "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=", + "dev": true + }, "electron-to-chromium": { "version": "1.3.18", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.18.tgz", @@ -2622,6 +2596,12 @@ "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" }, + "filesize": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", + "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", + "dev": true + }, "fill-range": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", @@ -3643,6 +3623,15 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, "hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -5351,6 +5340,12 @@ "wrappy": "1.0.2" } }, + "opener": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", + "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=", + "dev": true + }, "opn": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", @@ -6334,15 +6329,6 @@ "react-transition-group": "1.2.0" } }, - "react-chartjs-2": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.6.2.tgz", - "integrity": "sha512-9tyl0lFKJw3o9b6o1zFgwMN2D5jhQvt+bs6vQTEFvdqi4LDM2m2LiNgbCdPgnrwYslfZ40F6/yR2I6nREEt8tQ==", - "requires": { - "lodash": "4.17.4", - "prop-types": "15.5.10" - } - }, "react-d3-graph": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/react-d3-graph/-/react-d3-graph-0.2.1.tgz", @@ -7928,6 +7914,12 @@ } } }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, "union-value": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", @@ -8301,6 +8293,25 @@ } } }, + "webpack-bundle-analyzer": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.1.tgz", + "integrity": "sha512-a+UcvlsXvCmclNgfThT8PVyuJKd029By7CxkYEbNNCfs0Lqj9gagjkdv3S3MBvCIKBaUGYs8l4UpiVI0bFoh2Q==", + "dev": true, + "requires": { + "acorn": "5.1.1", + "chalk": "1.1.3", + "commander": "2.11.0", + "ejs": "2.5.7", + "express": "4.15.4", + "filesize": "3.5.11", + "gzip-size": "3.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "opener": "1.4.3", + "ws": "3.3.2" + } + }, "webpack-dev-middleware": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz", @@ -8599,6 +8610,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "ws": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", + "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, "xml-char-classes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", diff --git a/app/package.json b/app/package.json index 5302270..2aabf30 100644 --- a/app/package.json +++ b/app/package.json @@ -7,7 +7,6 @@ }, "dependencies": { "@blueprintjs/core": "^1.25.0", - "@types/chart.js": "^2.6.2", "@types/csv-parse": "^1.1.11", "@types/d3": "^4.10.0", "@types/lodash": "^4.14.71", @@ -24,7 +23,6 @@ "@types/redux-logger": "^3.0.0", "@types/webpack-env": "^1.13.2", "awesome-typescript-loader": "^3.2.2", - "chart.js": "^2.6.0", "classnames": "^2.2.5", "css-loader": "^0.28.4", "csv-parse": "^2.0.0", @@ -39,7 +37,6 @@ "morgan": "^1.8.2", "react": "^15.6.1", "react-addons-css-transition-group": "^15.6.0", - "react-chartjs-2": "^2.6.2", "react-d3-graph": "^0.2.1", "react-dom": "^15.6.1", "react-graph-vis": "^1.0.1", @@ -55,7 +52,11 @@ "typescript": "^2.4.2", "uglifyjs-webpack-plugin": "^1.0.0-beta.3", "webpack": "^3.4.1", + "webpack-bundle-analyzer": "^2.9.1", "webpack-dev-server": "^2.6.1", "whatwg-fetch": "^2.0.3" + }, + "devDependencies": { + } } diff --git a/app/src/application/actions.tsx b/app/src/application/actions.tsx index aa4255f..7632333 100644 --- a/app/src/application/actions.tsx +++ b/app/src/application/actions.tsx @@ -6,10 +6,24 @@ import { import config from './config' const serverUrl = config('app').serverURL -export function changeSelectedConceptNav(conceptNode: any): action { +export function changeSelectedRootNav(node: any): action { return { - type: 'CHANGE_SELECTED_CONCEPT_NAV', - value: conceptNode, + type: 'CHANGE_SELECTED_ROOT_NAV', + value: node, + } +} + +export function changeSelectedNodeNav(node: any): action { + return { + type: 'CHANGE_SELECTED_NODE_NAV', + value: node, + } +} + +export function changeDisplayedConceptNav(node: any): action { + return { + type: 'CHANGE_DISPLAYED_CONCEPT_NAV', + value: node, } } diff --git a/app/src/application/components/d3Blocks/conceptGraph.tsx b/app/src/application/components/d3Blocks/conceptGraph.tsx index db3fb0d..19fbf36 100644 --- a/app/src/application/components/d3Blocks/conceptGraph.tsx +++ b/app/src/application/components/d3Blocks/conceptGraph.tsx @@ -176,7 +176,7 @@ export class ConceptGraph extends React.Component { customDoubleClick(d: d3GraphNode) { // It is important that this action is dispatched first as it erases // the list of displayed slugs from the Redux state. - this.props.dispatch(actions.changeSelectedConceptNav(d)) + this.props.dispatch(actions.changeDisplayedConceptNav(d)) // TODO: associate correct container instead of default cp1 this.props.dispatch(actions.fetchConcept('concepts/' + d.slug, 'cp1')) this.props.dispatch(actions.toggleNavPanel()) diff --git a/app/src/application/components/d3Blocks/conceptHierarchy.less b/app/src/application/components/d3Blocks/conceptHierarchy.less new file mode 100644 index 0000000..2f1cba7 --- /dev/null +++ b/app/src/application/components/d3Blocks/conceptHierarchy.less @@ -0,0 +1,81 @@ +@import '~@blueprintjs/core/dist/variables.less'; + +svg#conceptHierarchy { + background-color: @dark-gray2; +} + +#container { + vertical-align: baseline; +} + +rect { + stroke-width: 0; +} + +.breadcrumbs { + margin: 20px 10px 10px 20px; + + li { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } +} + +.hierarchy-rect { + fill: @gray3; +} + +.hierarchy-rect:hover { + fill: @gray2; +} + +.ancestor-rect { + stroke: @green2; + stroke-width: 4; +} + +.ancestor-rect:hover { + stroke: @green1; +} + +.selected-rect { + fill: @green2; +} + +.selected-rect:hover { + fill: @green1; +} + +.dot-line { + stroke: @gray2; + stroke-width: 1; + stroke-dasharray: 3, 2; +} + +#tooltip { + pointer-events: none; + + text { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } +} + +#tooltip_content { + fill: @light-gray1; + font-size: 15px; + text-anchor: middle; + + tspan { + alignment-baseline: hanging; + } +} + +#tooltip_background, #tooltip_pointer { + fill: black; + opacity: 0.85; +} diff --git a/app/src/application/components/d3Blocks/conceptHierarchy.tsx b/app/src/application/components/d3Blocks/conceptHierarchy.tsx new file mode 100644 index 0000000..0082337 --- /dev/null +++ b/app/src/application/components/d3Blocks/conceptHierarchy.tsx @@ -0,0 +1,612 @@ +import * as _ from 'lodash' +import * as d3 from 'd3' +import * as React from 'react' +import * as ReactDOM from 'react-dom' + +import './conceptHierarchy.less' + +import * as actions from '../../actions' +import * as interceptClick from './utils/interceptClick' +import { + action, + conceptLinksAttribute, + extendedConceptNodeAttribute, +} from './../../types' +import * as wrap from './utils/wrap' + +interface d3GraphNode extends d3.SimulationNodeDatum, extendedConceptNodeAttribute {} + +interface d3GraphLink extends d3.SimulationNodeDatum, conceptLinksAttribute {} + +interface Props { + version: number, + nodes: extendedConceptNodeAttribute[], + graph: any, + selectedRoot: any, + selectedNode: any, + dimensions: { + width: number, + height: number, + } + searchedConcept: string, + dispatch: any +} + +interface State { + selected: string, +} + +export function ConceptHierarchyReducer(nodes: any): any { + let nodeDict: any = {} + let graph: any = [] + + function searchDict(slug: any): any { + if (slug in nodeDict) { + let r = searchDict(nodeDict[slug]) + nodeDict[slug] = r + return r + } else { + return slug + } + } + + let cc = 0 + nodes.forEach((node: any) => { + if (node.parent) { + nodeDict[node.slug] = searchDict(node.parent) + } else { + nodeDict[node.slug] = cc + cc++ + } + }) + + function recursiveCC(slug: any): any { + if (typeof nodeDict[slug] === 'number') { + return nodeDict[slug] + } else { + let r = recursiveCC(nodeDict[slug]) + nodeDict[slug] = r + return r + } + } + + for (let slug in nodeDict) { + nodeDict[slug] = recursiveCC(slug) + } + + nodes = _.map(nodes, (node: any) => { + return { + ...node, + connexComponent: nodeDict[node.slug], + } + }) + + for (let i = 0; i < cc; i++) { + let n = _.filter(nodes, (node: any) => node.connexComponent == i) + let h = d3.stratify() + .id((d: any) => d.slug) + .parentId((d: any) => d.parent) + (n) + graph.push(h) + } + + let graphNodes: any = [] + graph.forEach((rootNode: any) => { + rootNode.each((node: any) => { + graphNodes.push(node) + }) + }) + + return { + nodes, + graph, + graphNodes, + } +} + +export class ConceptHierarchy extends React.Component { + // D3 + + // All these parameters are simple parameters; one doesn't use + // React props only since they imply abiding by the React lifecycle + // whereas we want to have our simulation ran by d3. + width: number + height: number + svgDimensions = { + m: { + t: 15, + r: 5, + b: 5, + l: 5, + } + } + rectDimensions = { + w: 55, + h: 30, + m: { + t: 10, + r: 5, + b: 10, + l: 5, + }, + rm: { + t: 10, + r: 15, + b: 10, + l: 15, + } + } + tooltipDimensions = { + w: 120, + h: 15, + p: { + t: 10, + r: 5, + b: 10, + l: 5, + }, + t: { + w: 12, + h: 12, + } + } + transitionDuration = 50 + + rebind = interceptClick.rebind + interceptClick = interceptClick.interceptClick + interceptClickHandlerRoot: any + interceptClickHandlerHierarchy: any + + domSvg: any + domHierarchy: any + domHierarchyRects: any + domTopTicks: any + domBottomTicks: any + domLines: any + domRoots: any + domRootRects: any + + hierarchy: any + nodes: any + graph: any + fnodes: any + + selectedRoot: any + selectedNode: any + + rectsPerLevel: any + indexParentPerLevel: any + selectedNodeAncestors: any + displayedNodes: any + xFactor: any + yFactor: any + xFactorRoot: any + yFactorRoot: any + depthIncrement: any + + initAttributes() { + this.interceptClickHandlerRoot = this.interceptClick() + this.interceptClickHandlerHierarchy = this.interceptClick() + } + + updateHierarchyNodes() { + this.fnodes = [] + + if (this.selectedRoot) { + this.selectedRoot.each((node: any) => { + this.fnodes.push(node) + }) + } + } + + updateAttributes() { + let {width, height} = this.props.dimensions + this.width = width + this.height = height + + this.nodes = this.props.nodes + this.graph = this.props.graph + + this.selectedRoot = this.props.selectedRoot + this.selectedNode = this.props.selectedNode + + this.updateHierarchyNodes() + + this.domHierarchy + .attr('transform', 'translate(' + this.width / 2 + ',' + this.height / 2 + ')') + + this.domRoots + .attr('transform', 'translate(' + this.width / 2 + ', 50)') + + this.domSvg.select('#tooltip_container') + .attr('transform', 'translate(' + this.width / 2 + ',' + this.height / 2 + ')') + .style('opacity', 0) + + this.domSvg.select('#breadcrumbs') + .attr('width', this.width) + .attr('height', '30px') + .attr('transform', 'translate(' + 0 + ',' + (this.height - 80) + ')') + } + + updateRenderingAttributes() { + this.rectsPerLevel = [1] + this.indexParentPerLevel = [0] + this.selectedNodeAncestors = this.selectedNode ? this.selectedNode.ancestors() : [] + + this.displayedNodes = _.reduce(_.map(this.selectedNodeAncestors.reverse(), (n: any) => n.children ? n.children : []), + (acc: any, list: any) => { + if (list.length > 0 && list[0].depth < this.selectedNodeAncestors.length) { + list.forEach((node: any, index: number) => { + if (node == this.selectedNodeAncestors[node.depth]) { + this.indexParentPerLevel.push(index) + } + }) + } + this.rectsPerLevel.push(list.length) + return acc.concat(list) + }, + [this.selectedRoot] + ) + + this.xFactor = (this.rectDimensions.w + this.rectDimensions.m.r + this.rectDimensions.m.l) + this.yFactor = (this.rectDimensions.h + this.rectDimensions.m.t + this.rectDimensions.m.b) + this.xFactorRoot = (this.rectDimensions.w + this.rectDimensions.rm.r + this.rectDimensions.rm.l) + this.yFactorRoot = (this.rectDimensions.h + this.rectDimensions.rm.t + this.rectDimensions.rm.b) + } + + restartDepthIncrement() { + this.depthIncrement = [] + for (let i = 0; i < this.rectsPerLevel.length; i++) { + this.depthIncrement.push(0) + } + } + + parseTranslateTransformation(inputText: string) { + let r = inputText.split('(') + if (r.length > 0) { + r = r[1].split(',') + return _.map(r, (t: string) => parseFloat(t)) + } + + return null + } + + handleMouseOver(that: any, d: any) { + // Place the tooltip in the correct container + let domParentId = d3.select(this as any).node().parentNode.id + + if (domParentId == 'hierarchy') { + that.domSvg.select('#tooltip_container') + .attr('transform', 'translate(' + that.width / 2 + ',' + that.height / 2 + ')') + .style('opacity', 0) + } else if (domParentId == 'roots') { + that.domSvg.select('#tooltip_container') + .attr('transform', 'translate(' + that.width / 2 + ', 50)') + .style('opacity', 0) + } + + let tag = this as any + let attribute = d3.select(tag).attr('transform') + let translate = that.parseTranslateTransformation(attribute) + + that.domSvg.select('#tooltip_container') + .style('opacity', 1) + + that.domSvg.select('#tooltip_content') + .text(d.data.name) + .call(_.partial(wrap.wrap, 120)) + + that.domSvg.select('#tooltip') + .attr('transform', () => { + let xShift = translate[0] + that.rectDimensions.w / 2 + let yShift = translate[1] - d3.selectAll('#tooltip_content tspan').size() * that.tooltipDimensions.h - that.tooltipDimensions.p.b - that.tooltipDimensions.t.h - 4 + return 'translate(' + xShift + ',' + yShift +')' + }) + + that.domSvg.select('#tooltip_background') + .attr('transform', 'translate(' + (- (that.tooltipDimensions.w + that.tooltipDimensions.p.r + that.tooltipDimensions.p.l) / 2) + ',' + (- that.tooltipDimensions.p.t) +')') + .attr('width', that.tooltipDimensions.w + that.tooltipDimensions.p.r + that.tooltipDimensions.p.l) + .attr('height', () => { + return (d3.selectAll('#tooltip_content tspan').size() * that.tooltipDimensions.h + + that.tooltipDimensions.p.t + that.tooltipDimensions.p.b) + }) + + that.domSvg.select('#tooltip_pointer') + .attr('transform', () => { + let xShift = - that.tooltipDimensions.t.w / 2 + let yShift = d3.selectAll('#tooltip_content tspan').size() * that.tooltipDimensions.h + that.tooltipDimensions.p.b + return 'translate(' + xShift + ',' + yShift +')' + }) + } + + handleMouseOut(that: any, d: any) { + that.domSvg.select('#tooltip_container') + .style('opacity', 0) + } + + customClick(newRootNode: any, node: any) { + if (newRootNode) { + this.selectedRoot = node + this.updateHierarchyNodes() + this.props.dispatch(actions.changeSelectedRootNav(node)) + } + + this.domSvg.select('#tooltip_container') + .style('opacity', 0) + + this.selectedNode = node + this.props.dispatch(actions.changeSelectedNodeNav(node)) + } + + customDoubleClick(node: any) { + this.selectedNode = node + this.props.dispatch(actions.changeSelectedNodeNav(node)) + + this.props.dispatch(actions.changeDisplayedConceptNav(node)) + // TODO: associate correct container instead of default cp1 + this.props.dispatch(actions.fetchConcept('concepts/' + node.data.slug, 'cp1')) + this.props.dispatch(actions.toggleNavPanel()) + } + + singleRectTranslation(d: any, index: number=null) { + let xShift = 0 + let yShift = this.yFactor * d.depth + + if (d.depth < this.rectsPerLevel.length && this.displayedNodes.indexOf(d) > -1) { + xShift = this.xFactor * (this.depthIncrement[d.depth] - (this.rectsPerLevel[d.depth] - 1) / 2) - this.rectDimensions.w / 2 + if (d.depth < this.indexParentPerLevel.length) { + xShift -= this.xFactor * (this.indexParentPerLevel[d.depth] - (this.rectsPerLevel[d.depth] - 1) / 2) + } + this.depthIncrement[d.depth]++ + } + + return 'translate(' + xShift + ',' + yShift +')' + } + + renderRectangles() { + // Draws root rectangles + let availableRoots = _.filter(this.graph, (root: any) => root != this.selectedRoot) + + this.domRootRects = this.domRoots.selectAll('rect') + .data(availableRoots, (node: any) => node.data.id) + + this.domRootRects.exit().remove() + + this.domRootRects + .enter() + .append('rect') + .attr('width', this.rectDimensions.w) + .attr('height', this.rectDimensions.h) + .on('mouseover', _.partial(this.handleMouseOver, this)) + .on('mouseout', _.partial(this.handleMouseOut, this)) + .merge(this.domRootRects) + .call(this.interceptClickHandlerRoot + .on('customClick', this.customClick.bind(this, true)) + .on('customDoubleClick', this.customDoubleClick.bind(this)) + ) + .transition() + .delay(this.transitionDuration) + .attr('transform', (d: any, index: number) => + 'translate(' + (this.xFactorRoot * (index - (availableRoots.length) / 2) + (this.rectDimensions.rm.r + this.rectDimensions.rm.l) / 2) + + ',' + this.yFactorRoot + ')' + ) + .attr('class', (node: any) => { + return 'hierarchy-rect' + + ((this.selectedNode && node.id == this.selectedNode.id) ? ' selected-rect' : '') + + ((this.selectedNodeAncestors.indexOf(node) > -1) ? ' ancestor-rect' : '') + }) + + // Draws hierarchy rectangles + this.domHierarchyRects = this.domHierarchy.selectAll('rect') + .data(this.fnodes, (node: any) => node.data.id) + + this.domHierarchyRects.exit().remove() + + this.domHierarchyRects + .enter() + .append('rect') + .attr('width', this.rectDimensions.w) + .attr('height', this.rectDimensions.h) + .merge(this.domHierarchyRects) + .call(this.interceptClickHandlerHierarchy + .on('customClick', this.customClick.bind(this, false)) + .on('customDoubleClick', this.customDoubleClick.bind(this)) + ) + .on('mouseover', _.partial(this.handleMouseOver, this)) + .on('mouseout', _.partial(this.handleMouseOut, this)) + .transition() + .delay(this.transitionDuration) + .attr('transform', (d: any) => this.singleRectTranslation(d)) + .attr('display', (d: any) => { + let shouldDisplay = this.displayedNodes.indexOf(d) > -1 + + return shouldDisplay ? null : 'none' + }) + .attr('class', (node: any) => { + return 'hierarchy-rect' + + ((this.selectedNode && node.id == this.selectedNode.id) ? ' selected-rect' : '') + + ((this.selectedNodeAncestors.indexOf(node) > -1) ? ' ancestor-rect' : '') + }) + } + + renderTopTicks() { + this.domTopTicks = this.domHierarchy.selectAll('.top-tick') + .data(this.fnodes) + + this.domTopTicks.exit().remove() + + this.domTopTicks + .enter() + .append('line') + .merge(this.domTopTicks) + .transition() + .delay(this.transitionDuration) + .attr('class', 'top-tick dot-line') + .attr('display', (d: any) => { + let shouldDisplay = this.displayedNodes.indexOf(d) > -1 && d.depth > 0 + + return shouldDisplay ? null : 'none' + }) + .attr('x1', (d: any, index: number) => this.rectDimensions.w / 2) + .attr('x2', (d: any, index: number) => this.rectDimensions.w / 2) + .attr('y1', (d: any, index: any) => -this.rectDimensions.m.t) + .attr('y2', (d: any, index: any) => 0) + .attr('transform', (d: any) => this.singleRectTranslation(d)) + } + + renderBottomTicks() { + this.domBottomTicks = this.domHierarchy.selectAll('.bottom-tick') + .data(this.fnodes) + + this.domBottomTicks.exit().remove() + + this.domBottomTicks + .enter() + .append('line') + .merge(this.domBottomTicks) + .transition() + .delay(this.transitionDuration) + .attr('class', 'bottom-tick dot-line') + .attr('display', (d: any) => { + let shouldDisplay = this.selectedNodeAncestors.indexOf(d) > -1 + if (d == this.selectedNode && (d.children == null || d.children.length == 0)) { + shouldDisplay = false + } + + return shouldDisplay ? null : 'none' + }) + .attr('x1', (d: any, index: number) => this.rectDimensions.w / 2) + .attr('x2', (d: any, index: number) => this.rectDimensions.w / 2) + .attr('y1', (d: any, index: any) => this.rectDimensions.h) + .attr('y2', (d: any, index: any) => this.rectDimensions.h + this.rectDimensions.m.b) + .attr('transform', (d: any) => this.singleRectTranslation(d)) + } + + renderHorizontalBars() { + let dataLines = this.rectsPerLevel + dataLines.shift() + if (this.selectedNode && this.selectedNode.depth > 0 && (this.selectedNode.children == null || this.selectedNode.children.length == 0)) { + dataLines.pop() + } + + this.domLines = this.domHierarchy.selectAll('.hbar') + .data(dataLines) + + this.domLines.exit().remove() + + this.domLines + .enter() + .append('line') + .merge(this.domLines) + .transition() + .delay(this.transitionDuration) + .attr('class', 'hbar dot-line') + .attr('x1', (d: any, index: number) => { + let xShift = 0 + if (index+1 < this.indexParentPerLevel.length) { + xShift += this.xFactor * (this.indexParentPerLevel[index+1] - (d-1) / 2) + } + return -((d-1) * this.xFactor / 2 + xShift) + }) + .attr('x2', (d: any, index: number) => { + let xShift = 0 + if (index+1 < this.indexParentPerLevel.length) { + xShift += this.xFactor * (this.indexParentPerLevel[index+1] - (d-1) / 2) + } + return (d-1) * this.xFactor / 2 - xShift + }) + .attr('y1', (d: any, index: any) => (index + 1) * this.yFactor - this.rectDimensions.m.t) + .attr('y2', (d: any, index: any) => (index + 1) * this.yFactor - this.rectDimensions.m.t) + } + + renderBreadcrumbs() { + const selectedNodePath = this.selectedNode ? this.selectedNode.ancestors().reverse() : [] + + let breads = this.domSvg.select('#breadcrumb-list') + .selectAll('li') + .data(selectedNodePath, (node: any) => node.data.id) + + breads.exit().remove() + breads.enter() + .append('li') + .attr('class', 'pt-breadcrumb') + .html((d: any) => d.data.name) + } + + renderAll() { + this.updateRenderingAttributes() + this.restartDepthIncrement() + this.renderRectangles() + this.restartDepthIncrement() + this.renderTopTicks() + this.restartDepthIncrement() + this.renderBottomTicks() + this.restartDepthIncrement() + this.renderHorizontalBars() + this.renderBreadcrumbs() + } + + renderD3DomElements() { + this.renderAll() + } + + // REACT LIFECYCLE + + constructor(props: Props) { + super(props) + this.state = {selected: null} + } + + componentDidMount() { + this.domSvg = d3.select(this.refs.containerHierarchy as any) + this.domHierarchy = this.domSvg.select('#hierarchy') + this.domRoots = this.domSvg.select('#roots') + + this.width = this.props.dimensions.width + this.height = this.props.dimensions.height + + this.initAttributes() + this.updateAttributes() + this.renderD3DomElements() + } + + componentDidUpdate() { + this.updateAttributes() + this.renderD3DomElements() + } + + render() { + let {width, height} = this.props.dimensions + + return ( + + +
+ +
+
+ + + + + + + + + +
+ ) + } +} diff --git a/app/src/application/components/d3Blocks/conceptNav.tsx b/app/src/application/components/d3Blocks/conceptNav.tsx index 0a16091..41f6784 100644 --- a/app/src/application/components/d3Blocks/conceptNav.tsx +++ b/app/src/application/components/d3Blocks/conceptNav.tsx @@ -27,7 +27,7 @@ interface Props { } graph: conceptGraph, nodes: extendedConceptNodeAttribute[], - selectedConceptNode: extendedConceptNodeAttribute, + displayedNode: any, displayedSlugs: string[], dispatch: any, } @@ -63,7 +63,7 @@ export class ConceptNav extends React.Component { if (!hNode.toggled) { hNode.toggled = true hNode.visible = true - hNode.children.forEach(this.hierachicalToggle.bind(this)) + hNode.children ? hNode.children.forEach(this.hierachicalToggle.bind(this)) : null } else { hNode.toggled = false hNode.each((hNode: extendedHierarchyNode) => { @@ -74,17 +74,17 @@ export class ConceptNav extends React.Component { } updateHierarchy() { - if (this.props.selectedConceptNode && this.props.selectedConceptNode.connexComponent) { + if (this.props.displayedNode && this.props.displayedNode.data.connexComponent) { this.graph = this.props.graph let selectedNode - if (this.selectedNodeId != this.props.selectedConceptNode.id) { - this.selectedNodeId = this.props.selectedConceptNode.id - this.hierarchy = this.graph[this.props.selectedConceptNode.connexComponent] + if (this.selectedNodeId != this.props.displayedNode.data.id) { + this.selectedNodeId = this.props.displayedNode.data.id + this.hierarchy = this.graph[this.props.displayedNode.data.connexComponent] this.hierarchy.each((hNode: extendedHierarchyNode) => { - if (hNode.data.id == this.props.selectedConceptNode.id) { + if (hNode.data.id == this.props.displayedNode.data.id) { selectedNode = hNode } }) @@ -216,13 +216,13 @@ export class ConceptNav extends React.Component { // REACT LIFECYCLE selectGraph(props: Props) { - let selectedGraph: extendedHierarchyNode + let selectedGraph: any - if (props.selectedConceptNode && props.selectedConceptNode.id) { - let hierarchy = props.graph[props.selectedConceptNode.connexComponent] + if (props.displayedNode && props.displayedNode.data.id) { + let hierarchy = props.graph[props.displayedNode.data.connexComponent] hierarchy.each((node: extendedHierarchyNode) => { - if (node.data.id == props.selectedConceptNode.id) { + if (node.data.id == props.displayedNode.data.id) { selectedGraph = node } }) diff --git a/app/src/application/components/d3Blocks/scatter.tsx b/app/src/application/components/d3Blocks/scatter.tsx index e7dffe6..cd89ac5 100644 --- a/app/src/application/components/d3Blocks/scatter.tsx +++ b/app/src/application/components/d3Blocks/scatter.tsx @@ -189,7 +189,7 @@ export class Scatter extends React.Component { .translateExtent([[0, 0], [this.lineDimensions.width, this.lineDimensions.height]]) .extent([[0, 0], [this.lineDimensions.width, this.lineDimensions.height]]) .on('zoom', this.zoomed.bind(this)) - + this.domAxes = this.domContainer .attr('width', this.lineDimensions.width + this.padding.left + this.padding.right) .attr('height', this.lineDimensions.height + this.padding.top + this.padding.bottom) diff --git a/app/src/application/components/d3Blocks/sunburst.less b/app/src/application/components/d3Blocks/sunburst.less index 4f1fbac..35dfcc1 100644 --- a/app/src/application/components/d3Blocks/sunburst.less +++ b/app/src/application/components/d3Blocks/sunburst.less @@ -6,7 +6,7 @@ path { stroke-opacity: 1; } -#breadcrumb { +#breadcrumbs { height: 18px; font-size: 15px; } diff --git a/app/src/application/components/d3Blocks/sunburst.tsx b/app/src/application/components/d3Blocks/sunburst.tsx index 10fd30d..c438ec7 100644 --- a/app/src/application/components/d3Blocks/sunburst.tsx +++ b/app/src/application/components/d3Blocks/sunburst.tsx @@ -54,7 +54,7 @@ export class Sunburst extends React.Component { .attr('height', this.props.dimensions.height) this.domContainer .attr('transform', 'translate(' + this.props.dimensions.width / 2 + ',' + this.props.dimensions.height / 2 + ')') - d3.select('#info') + this.domSvg.select('#info') .attr('transform', 'translate(' + this.props.dimensions.width / 2 + ',' + this.props.dimensions.height / 2 + ')') this.radius = Math.min(this.props.dimensions.width, this.props.dimensions.height) / 2 @@ -119,10 +119,9 @@ export class Sunburst extends React.Component { } handleMouseleave(d: any) { - d3.select('#breadcrumb') + d3.select('#main #breadcrumbs') .style('visibility', 'hidden') - d3.selectAll('path') .transition() .duration(150) @@ -133,18 +132,19 @@ export class Sunburst extends React.Component { } updateBreadcrumbs(nodeArray: any, percentageString: any) { - d3.select('#breadcrumb').style('visibility', '') + d3.select('#main #breadcrumbs').style('visibility', '') - let breads = d3.select('#breadcrumb-list') + let breads = d3.select('#main #breadcrumbs #breadcrumb-list') .selectAll('li') - .data(nodeArray, (d: any) => d.data.name + d.depth) + // .data(nodeArray, (d: any) => d.data.name + d.depth) + .data(nodeArray) breads.exit().remove() breads.enter() .append('li') .append('a') .attr('class', 'pt-breadcrumb') - .html((d) => d.data.name) + .html((d: any) => d.data.name) } buildHierarchy(csv: any) { @@ -216,7 +216,6 @@ export class Sunburst extends React.Component { } componentDidUpdate() { - console.log('update') this.updateAttributes() let text = ` orion-sunburst-redo,120 @@ -238,22 +237,22 @@ export class Sunburst extends React.Component { return (
- diff --git a/app/src/application/containers/conceptsPresentation.tsx b/app/src/application/containers/conceptsPresentation.tsx index 806afac..4c44fb0 100644 --- a/app/src/application/containers/conceptsPresentation.tsx +++ b/app/src/application/containers/conceptsPresentation.tsx @@ -17,7 +17,6 @@ import * as actions from '../actions' import {TimeseriesChart} from '../components/modules/timeseries_chart' import {HistogramChart} from '../components/modules/histogram_chart' -import {DoughnutChart} from '../components/modules/labelized_chart' import {Definition} from '../components/modules/definition' import {Suggestion} from '../components/modules/suggestion' @@ -87,11 +86,6 @@ export class ConceptsPresentation extends React.Component { this.props.dispatch(actions.fetchConcept('concepts/' + slug, this.props.containerId)) }} /> - case 'doughnut': - return default: return
{m.type}
} diff --git a/app/src/application/containers/test.tsx b/app/src/application/containers/test.tsx index b8d58d5..f76f76f 100644 --- a/app/src/application/containers/test.tsx +++ b/app/src/application/containers/test.tsx @@ -42,8 +42,8 @@ export class Test extends React.Component { // When page is done loading, fetch concept graph from backend componentDidMount() { - // this.props.dispatch(actions.fetchConceptGraph('concepts/', 'test')) - this.props.dispatch(actions.testFetch(['PLF2017-Nomenclature_MPA.csv'], 'test', 'http://localhost:31338/')) + this.props.dispatch(actions.fetchConceptGraph('concepts/', 'test')) + // this.props.dispatch(actions.testFetch(['PLF2017-Nomenclature_MPA.csv'], 'test', 'http://localhost:31338/')) this.props.dispatch(actions.toggleNavPanel()) } @@ -87,11 +87,13 @@ export class Test extends React.Component { >
@@ -115,7 +117,6 @@ export class Test extends React.Component { />
- diff --git a/app/src/application/reducers.tsx b/app/src/application/reducers.tsx index 6d00b1d..6581c94 100644 --- a/app/src/application/reducers.tsx +++ b/app/src/application/reducers.tsx @@ -10,8 +10,8 @@ import { } from './types' import {TimeseriesValuesReducer} from './components/modules/timeseries_chart' -import {LabelizedValuesReducer} from './components/modules/labelized_chart' import {navPanelReducer} from './components/utils/navPanel' +import {ConceptHierarchyReducer} from './components/d3Blocks/conceptHierarchy' const initialCp1State: containerState = { containerId: 'cp1', @@ -26,16 +26,18 @@ const initialAppContainerState: containerState = { const initialTestState: containerState = { containerId: 'test', + concepts: [], loading: 0, } const initialAppState: appState = { conceptGraph: { nodes: [], - links: [], - suggestedLinks: [], graph: {}, - selectedConceptNode: null, + graphNodes: [], + selectedRoot: null, + selectedNode: null, + displayedNode: null, displayedSlugs: [], }, containers: { @@ -88,12 +90,32 @@ export function reducer(state = initialAppState, action: action): appState { } } - case 'CHANGE_SELECTED_CONCEPT_NAV': + case 'CHANGE_SELECTED_ROOT_NAV': + return { + ...state, + conceptGraph: { + ...state.conceptGraph, + selectedRoot: action.value, + }, + containers: initialAppState.containers, + } + + case 'CHANGE_SELECTED_NODE_NAV': + return { + ...state, + conceptGraph: { + ...state.conceptGraph, + selectedNode: action.value, + }, + containers: initialAppState.containers, + } + + case 'CHANGE_DISPLAYED_CONCEPT_NAV': return { ...state, conceptGraph: { ...state.conceptGraph, - selectedConceptNode: action.value, + displayedNode: action.value, displayedSlugs: [], }, containers: initialAppState.containers, @@ -137,8 +159,6 @@ export function reducer(state = initialAppState, action: action): appState { } case 'timeseries': return TimeseriesValuesReducer(m) - case 'labelizedvalues': - return LabelizedValuesReducer(m) default: return m } @@ -179,16 +199,15 @@ export function reducer(state = initialAppState, action: action): appState { } case 'FETCH_CONCEPT_GRAPH_SUCCESS': - let {nodes, links, suggestedLinks, graph} = navPanelReducer(action) + let {nodes, graph, graphNodes} = ConceptHierarchyReducer(action.value.nodes) return { ...state, conceptGraph: { ...state.conceptGraph, - nodes, - links, - suggestedLinks, + nodes: nodes, graph, + graphNodes, }, containers: { ...state.containers, diff --git a/app/src/application/types.tsx b/app/src/application/types.tsx index 3c207e0..7a12c17 100644 --- a/app/src/application/types.tsx +++ b/app/src/application/types.tsx @@ -37,10 +37,11 @@ export interface appState { }, conceptGraph: { nodes: extendedConceptNodeAttribute[], - links: conceptLinksAttribute[], - suggestedLinks: conceptLinksAttribute[], - graph: conceptGraph, - selectedConceptNode: any, + graph?: any, + graphNodes: any, + selectedRoot: any, + selectedNode: any, + displayedNode: any, displayedSlugs: string[], } dispatch?: any, diff --git a/app/src/style.less b/app/src/style.less index 53f8a72..46b823f 100644 --- a/app/src/style.less +++ b/app/src/style.less @@ -4,6 +4,27 @@ body { background-color: @dark-gray1; } +.concept-search-results > ul { + list-style-type: none; + margin: 0; + padding: 0; + + > li { + background-color: @dark-gray2; + padding: 10px 20px 10px 20px; + margin: 5px 5px 5px 5px; + } + + > li:hover { + background-color: @dark-gray3; + cursor: pointer; + } + + > li * { + pointer-events: none; + } +} + .pt-dark .pt-navbar, .pt-navbar { background-color: @dark-gray1; box-shadow: none; @@ -61,7 +82,7 @@ body { } .concept_nav_bar { - max-width: 600px; + max-width: 800px; margin: 10px auto 10px auto; } diff --git a/app/webpack.config.js b/app/webpack.config.js index 0a28d35..9e09d20 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -2,7 +2,8 @@ var webpack = require('webpack'), path = require('path'), htmlPlugin = require('html-webpack-plugin'), extractTextWebpackPlugin = require('extract-text-webpack-plugin'), - UglifyJSPlugin = require('uglifyjs-webpack-plugin') + UglifyJSPlugin = require('uglifyjs-webpack-plugin'), + BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin var SRC_DIR = path.join(__dirname, './src') @@ -16,15 +17,20 @@ module.exports = { entry: { main: './application/main.tsx', bundleLibraries: [ - 'chart.js', + '@blueprintjs/core', + 'csv-parse', 'd3', 'lodash', + 'moment', 'react', 'react-dom', + 'react-measure', 'react-redux', 'react-router', + 'react-router-dom', 'redux', - '@blueprintjs/core' + 'redux-logger', + 'slug', ] }, devtool: 'source-map', @@ -55,9 +61,18 @@ module.exports = { } ], }, - plugins: ((process.env.NODE_ENV == 'production') ? [new UglifyJSPlugin()] : []).concat([ + plugins: ( + (process.env.NODE_ENV == 'production') ? + [new UglifyJSPlugin()] : + [ + // Use when bundle analysis is needed: + // new BundleAnalyzerPlugin() + ] + ).concat([ // Plugin to compile libraries speficied in the entry // into a loadable bundle file. + new webpack.IgnorePlugin(/unicode\/category\/So/, /node_modules/), + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new webpack.optimize.CommonsChunkPlugin({ name: 'bundleLibraries', filename: 'libraries.bundle.js', diff --git a/data/concepts/budget.js b/data/concepts/budget.js index d4ff60e..f192b76 100644 --- a/data/concepts/budget.js +++ b/data/concepts/budget.js @@ -4,9 +4,6 @@ exports.node = { { type: 'definition', data_identifiers: ['budget'], - },{ - type: 'doughnut', - data_identifiers: ['plf_2017'], } ] } diff --git a/data/concepts/pib.js b/data/concepts/pib.js index 7328579..1156dbc 100644 --- a/data/concepts/pib.js +++ b/data/concepts/pib.js @@ -1,10 +1,6 @@ exports.node = { name: 'PIB (produit intérieur brut)', modules: [ - { - type: 'doughnut', - data_identifiers: ['pib_world_2016'], - } ] } diff --git a/data/schema.js b/data/schema.js index 1a0a17d..f36a3a7 100644 --- a/data/schema.js +++ b/data/schema.js @@ -6,7 +6,6 @@ const DB_CONFIG = require(path.join(__dirname, '../config')).default('database') const possibleModules = [ 'timeseries', 'definition', - 'doughnut' ] const possibleDatasets = [ diff --git a/package-lock.json b/package-lock.json index 86d6a9f..9794bbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -146,6 +146,11 @@ "@types/mime": "2.0.0" } }, + "@types/slug": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/slug/-/slug-0.9.0.tgz", + "integrity": "sha1-jYurp+SsvqLRmeje5mMwg+qJ9/g=" + }, "@types/superagent": { "version": "3.5.6", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.5.6.tgz", @@ -198,11 +203,6 @@ "@types/vinyl": "2.0.1" } }, - "@types/webpack-env": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.2.tgz", - "integrity": "sha512-pjbzi3A1Y4iLpNdNZNG4loIZKtYOnpCQY82bnsHi9lQXl4f3ul0TDsd1fUd10jbgCXR5bwaP4Ffy1BDLuEZpaQ==" - }, "accepts": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", @@ -4085,13 +4085,13 @@ } }, "sequelize-auto": { - "version": "git+https://github.com/sequelize/sequelize-auto.git#fd2add3b8b5020c7a5fdcb15279cbb1302771b95", + "version": "git+https://github.com/sequelize/sequelize-auto.git#40dfce6352eefe7fa8dec1c71dd5127c37ac0a52", "requires": { "async": "2.5.0", "eslint": "4.8.0", "graceful-fs-extra": "2.0.0", "mkdirp": "0.5.1", - "sequelize": "3.30.4", + "sequelize": "3.31.1", "yargs": "8.0.2" }, "dependencies": { @@ -4116,9 +4116,9 @@ "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "sequelize": { - "version": "3.30.4", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-3.30.4.tgz", - "integrity": "sha1-vaLfHjGFSwmeQUmhEen8Clyh0aQ=", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-3.31.1.tgz", + "integrity": "sha512-fRb2cu3d+A7wwBuHmfe+SP5nhkQ9pDS3nR/KBmjRK0EpBYRJmNggOqfEShuBgX+QztjXLFiemnT0AtjykfVUGw==", "requires": { "bluebird": "3.5.1", "depd": "1.1.1", @@ -4297,6 +4297,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -4321,11 +4326,6 @@ } } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/package.json b/package.json index 6362983..b4e19ba 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@types/gulp": "^4.0.4", "@types/mocha": "^2.2.43", "@types/sequelize": "^4.0.69", + "@types/slug": "^0.9.0", "@types/supertest": "^2.0.3", - "@types/webpack-env": "^1.13.2", "body-parser": "^1.17.2", "chai": "^4.1.2", "del": "^3.0.0", diff --git a/server/src/backends/concepts/backend.ts b/server/src/backends/concepts/backend.ts index 1790d91..58232e4 100644 --- a/server/src/backends/concepts/backend.ts +++ b/server/src/backends/concepts/backend.ts @@ -9,6 +9,7 @@ import { suggestion_valuesAttribute, } from '../../../../models/db' import { + sequelize, ConceptNodes, ConceptLinks, ConceptSuggestedLinks, @@ -32,31 +33,20 @@ export class ConceptBackend { // Create public Router this.router = Router({mergeParams: true}); - // Init all end points of the Router this.router.route('/') .get(async (request: Request, response: Response) => { // Get flat results (Sequelize normally returns complex Instance objects // which are later parsed by express when calling response.json() - const nodes = await ConceptNodes.findAll({ - ...options, - }); - const links = await ConceptLinks.findAll({ - ...options, - }); - const suggestedLinks = await ConceptSuggestedLinks.findAll({ - ...options, - }); - - // Promise.all() turns an array of Promises into actual values. - // const enrichedConcepts = await Promise.all( - // // TODO: En fait je ne comprends même pas pourquoi il faut bind ici :D - // concepts.map(this.enrichConcept.bind(this)) - // ) + const nodes = await sequelize + .query( + 'SELECT n.*, l.slug_to AS parent FROM concept_nodes n LEFT JOIN concept_links l ON n.slug = l.slug_from', + { + type: sequelize.QueryTypes.SELECT + } + ) response.json({ nodes, - links, - suggestedLinks, }); }) @@ -148,26 +138,6 @@ export class ConceptBackend { ...options, }) - data.push({ - info, - values, - }) - break - case 'doughnut': - var info = await Datasets.findOne({ - where: { - name: data_identifier - }, - ...options, - }) - - var values = await LabelizedValues.findAll({ - where: { - dataset: data_identifier - }, - ...options, - }) - data.push({ info, values, diff --git a/test/types.ts b/test/types.ts index 0851fa9..4cff335 100644 --- a/test/types.ts +++ b/test/types.ts @@ -32,15 +32,6 @@ describe('API RETURN TYPES', () => { }) }) - it('End-point concepts/ should return correctly typed links', () => { - return request(server) - .get('/concepts') - .expect((response: Response) => { - let data = JSON.parse(JSON.stringify(response.body)) - assert(data.links.filter((link: any) => isConceptLinksAttributes(link)).length == data.links.length, "Returned JSON is of wrong type, check integrity in type guard function isConceptLinksAttributes") - }) - }) - it('Test each slug integrity', async () => { let r = await request(server) .get('/concepts')