Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
595c4bd5a8 feat(computed): decorator for computed props 2018-01-24 14:35:00 +01:00
4 changed files with 159 additions and 190 deletions

126
src/common/computed.js Normal file
View File

@@ -0,0 +1,126 @@
import React, { PureComponent } from 'react'
const {
create,
defineProperty,
defineProperties,
getOwnPropertyDescriptors = obj => {
const descriptors = {}
const { getOwnPropertyDescriptor } = Object
for (const prop in obj) {
const descriptor = getOwnPropertyDescriptor(obj, prop)
if (descriptor !== undefined) {
descriptors[prop] = descriptor
}
}
return descriptors
},
prototype: { hasOwnProperty },
} = Object
const makePropsSpy =
typeof Proxy !== 'undefined'
? (obj, spy) =>
new Proxy(obj, {
get: (target, property) => (spy[property] = target[property]),
})
: (obj, spy) => {
const descriptors = {}
const props = getOwnPropertyDescriptors(obj)
for (const prop in props) {
const { configurable, enumerable, get, value } = props[prop]
descriptors[prop] = {
configurable,
enumerable,
get:
get !== undefined
? () => (spy[prop] = get.call(obj))
: () => (spy[prop] = value),
}
}
return create(null, descriptors)
}
// Decorator which provides computed properties for React components.
//
// ```js
// const MyComponent = computed({
// fullName: ({ firstName, lastName }) => `${lastName}, ${firstName}`
// })(({ fullName }) =>
// <p>{fullName}</p>
// )
// ```
const computed = computed => Component =>
class extends PureComponent {
constructor () {
super()
this._computedCache = create(null)
this._computedDeps = create(null)
const descriptors = (this._descriptors = {})
for (const name in computed) {
if (!hasOwnProperty.call(computed, name)) {
continue
}
const transform = computed[name]
let running = false
descriptors[name] = {
configurable: true,
enumerable: true,
get: () => {
// this is necessary to allow a computed value to depend on
// itself
if (running) {
console.log(name, 'running')
return this.props[name]
}
const cache = this._computedCache
const deps = this._computedDeps
const dependencies = deps[name]
let needsRecompute = dependencies === undefined
if (!needsRecompute) {
const { props } = this
for (const depName in dependencies) {
const value =
depName === name || !(depName in cache)
? props[depName]
: cache[depName]
needsRecompute = value !== dependencies[depName]
if (needsRecompute) {
break
}
}
}
console.log(name, needsRecompute)
if (needsRecompute) {
running = true
cache[name] = transform(
makePropsSpy(this._props, (deps[name] = create(null)))
)
running = false
}
const value = cache[name]
defineProperty(this._props, name, {
enumerable: true,
value,
})
return value
},
}
}
}
render () {
this._props = defineProperties({ ...this.props }, this._descriptors)
return <Component {...this._props} />
}
}
export { computed as default }

View File

@@ -9,6 +9,7 @@ import { getUser } from 'selectors'
import { serverVersion } from 'xo'
import { Container, Row, Col } from 'grid'
import { connectStore, getXoaPlan } from 'utils'
import computed from 'computed'
import pkg from '../../../package'
@@ -25,6 +26,20 @@ const HEADER = (
</Container>
)
let MyComponent = _ => <p>{_.firstName}</p>
MyComponent = computed({
firstName: ({ firstName }) => firstName.toUpperCase(),
fullName: _ =>
_.firstName === 'Bob'
? 'Bobinette'
: `${_.title} ${_.lastName}, ${_.firstName}`,
})(MyComponent)
MyComponent = computed({
title: () => 'Mr',
})(MyComponent)
@connectStore(() => ({
user: getUser,
}))
@@ -34,12 +49,30 @@ export default class About extends Component {
this.setState({ serverVersion })
})
}
state = {
firstName: 'John',
lastName: 'Smith',
};
render () {
const { user } = this.props
const isAdmin = user && user.permission === 'admin'
return (
<Page header={HEADER} title='aboutPage' formatTitle>
<input
value={this.state.firstName}
onChange={this.linkState('firstName')}
/>
<input
value={this.state.lastName}
onChange={this.linkState('lastName')}
/>
<MyComponent
firstName={this.state.firstName}
lastName={this.state.lastName}
/>
<Container className='text-xs-center'>
{isAdmin && (
<Row>

View File

@@ -25,7 +25,6 @@ import Backup from './backup'
import Dashboard from './dashboard'
import Home from './home'
import Host from './host'
import InnovativeTree from './innovative-tree'
import Jobs from './jobs'
import Menu from './menu'
import Modal, { alert } from 'modal'
@@ -91,7 +90,6 @@ const BODY_STYLE = {
'vms/:id': Vm,
xoa: Xoa,
xosan: Xosan,
'tree': InnovativeTree
})
@connectStore(state => {
return {

View File

@@ -1,188 +0,0 @@
import React, {Component} from 'react'
import * as d3 from 'd3'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import { createGetObjectsOfType } from 'selectors'
import { connectStore } from 'utils'
const COLORS = {
'VM': 'green',
host: 'red',
pool: 'blue'
}
@connectStore(() => {
const getVms = createGetObjectsOfType('VM')
const getPools = createGetObjectsOfType('pool')
const getHosts = createGetObjectsOfType('host')
return {
vms: getVms,
pools: getPools,
hosts: getHosts
}
})
class CanvasComponent extends React.Component {
componentDidMount = () => {
this.updateCanvas()
}
componentDidUpdate = () => {
this.updateCanvas()
}
constructTree = () => {
const {hosts, pools, vms} = this.props
const vmsGroupedByHost = []
forEach(vms, vm => {
const vmInformations = {id: vm.uuid, name_label: vm.name_label, object: vm, type: vm.type}
if (vmsGroupedByHost.size === 0) {
vmsGroupedByHost.push([{object: vm, ...vmInformations}])
} else {
var added = false
forEach(vmsGroupedByHost, group => {
if (group[0].object.$container === vm.$container) {
group.push({object: vm, ...vmInformations})
added = true
}
})
if (!added) {
vmsGroupedByHost.push([{object: vm, ...vmInformations}])
}
}
})
const hostArray = []
forEach(hosts, host => {
const children = find(vmsGroupedByHost, group => group[0].object.$container === host.uuid)
const hostInformations = {id: host.uuid, name_label: host.name_label, object: host, type: host.type}
if (children === undefined) {
hostArray.push({children: [], ...hostInformations})
} else {
hostArray.push({children, ...hostInformations})
}
})
const poolsArray = []
forEach(pools, pool => {
const objectPool = {id: pool.uuid, children: [], object: pool, name_label: pool.name_label, type: pool.type}
forEach(hostArray, host => {
if (objectPool.id === host.object.$pool) {
objectPool.children.push(host)
}
})
poolsArray.push(objectPool)
})
return {children: poolsArray, id: 'xoApp', name_label: 'xoApp'}
}
updateCanvas () {
const context = this.refs.canvas.getContext('2d')
const canvas = this.refs.canvas
const width = canvas.width
const height = canvas.height
var root = d3.hierarchy(this.constructTree())
var displayingText = false
var nodes = root.descendants()
var links = root.links()
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('link', d3.forceLink(links).strength(1))
.force('x', d3.forceX())
.force('y', d3.forceY())
.on('tick', ticked)
d3.select(canvas)
.call(d3.drag()
.container(canvas)
.subject(dragsubject)
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('mousemove', displayName)
function ticked () {
context.clearRect(0, 0, width, height)
context.save()
context.translate(width / 2, height / 2)
context.beginPath()
links.forEach(drawLink)
context.strokeStyle = '#aaa'
context.stroke()
nodes.forEach(drawNode)
context.restore()
}
function displayName () {
const limit = 5
const cursorX = d3.event.layerX - width / 2 - d3.event.target.offsetLeft
const cursorY = d3.event.layerY - height / 2 - d3.event.target.offsetTop
const node = find(nodes, n => n.x <= cursorX + limit && n.x >= cursorX - limit && n.y <= cursorY + limit && n.y >= cursorY - limit)
if (node !== undefined && !displayingText) {
context.fillText(node.data.name_label, 50, 50)
displayingText = true
} else if (node === undefined) {
displayingText = false
ticked()
}
}
function dragsubject () {
return simulation.find(d3.event.x - width / 2, d3.event.y - height / 2)
}
function dragstarted () {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
d3.event.subject.fx = d3.event.subject.x
d3.event.subject.fy = d3.event.subject.y
}
function dragged () {
d3.event.subject.fx = d3.event.x
d3.event.subject.fy = d3.event.y
}
function dragended () {
if (!d3.event.active) simulation.alphaTarget(0)
d3.event.subject.fx = null
d3.event.subject.fy = null
}
function drawLink (d) {
context.moveTo(d.source.x, d.source.y)
context.lineTo(d.target.x, d.target.y)
}
function drawNode (d) {
context.beginPath()
if (d.data.type !== undefined) {
context.fillStyle = COLORS[d.data.type]
} else {
context.fillStyle = 'black'
}
context.moveTo(d.x + 3, d.y)
context.arc(d.x, d.y, 8, 0, 2 * Math.PI)
context.fill()
}
}
render () {
return (
<canvas ref='canvas' width={500} height={500} />
)
}
}
export default class InnovativeTree extends Component {
render () {
return (
<div>
<h1>Innovative Tree</h1>
<CanvasComponent />
</div>
)
}
}