Add mock api json-server, update dashboard page with functionality to increase/decrease number of replicas and add service info page

This commit is contained in:
Andres Martin 2017-09-19 23:31:05 +09:00
parent ed609dc81f
commit e9f895bb47
10 changed files with 1823 additions and 277 deletions

13
ui/.babelrc Normal file
View File

@ -0,0 +1,13 @@
{
"presets": [
"next/babel",
],
"env": {
"development": {
"plugins": ["inline-dotenv"]
},
"production": {
"plugins": ["transform-inline-environment-variables"]
}
}
}

1
ui/.env Normal file
View File

@ -0,0 +1 @@
API_HOST=http://localhost:3001

View File

@ -1,3 +1,11 @@
# Orbiter UI
User interface built with React + Next.js
# Mock Api
* /v1/orbiter/autoscaler [GET]
* /v1/orbiter/events [GET]
* /v1/orbiter/handle/{autoscaler_name}/{service_name}/inspect-service [GET]
* /v1/orbiter/handle/{autoscaler_name}/{service_name}/{direction} [POST]
* /v1/orbiter/health [GET]

205
ui/api/db.json Normal file
View File

@ -0,0 +1,205 @@
{
"autoscaler": {
"data": [
{
"name": "autoswarm/frontend",
"replicas": 4
},
{
"name": "autoswarm/backend",
"replicas": 8
},
{
"name": "autoswarm/gateway",
"replicas": 2
},
{
"name": "autoswarm/monitoring",
"replicas": 1
}
]
},
"health": {
"status": true,
"info": {}
},
"events": {
},
"services-inspect": [
{
"id": "frontend",
"version": {
"index": 418
},
"createdAt": "2016-06-16T21:57:11.622222327Z",
"updatedAt": "2016-06-16T21:57:11.622222327Z",
"spec": {
"name": "frontend",
"taskTemplate": {
"containerSpec": {
"image": "alpine",
"args": [
"ping",
"docker.com"
]
},
"resources": {
"limits": {},
"reservations": {}
},
"restartPolicy": {
"condition": "any",
"maxAttempts": 0
},
"placement": {}
},
"mode": {
"replicated": {
"replicas": 4
}
},
"updateConfig": {
"parallelism": 1
},
"endpointSpec": {
"mode": "vip"
}
},
"endpoint": {
"spec": {}
}
},
{
"id": "backend",
"version": {
"index": 48
},
"createdAt": "2016-06-16T21:57:11.622222327Z",
"updatedAt": "2016-06-16T21:57:11.622222327Z",
"spec": {
"name": "backend",
"taskTemplate": {
"containerSpec": {
"image": "alpine",
"args": [
"ping",
"docker.com"
]
},
"resources": {
"limits": {},
"reservations": {}
},
"restartPolicy": {
"condition": "any",
"maxAttempts": 0
},
"placement": {}
},
"mode": {
"replicated": {
"replicas": 8
}
},
"updateConfig": {
"parallelism": 1
},
"endpointSpec": {
"mode": "vip"
}
},
"endpoint": {
"spec": {}
}
},
{
"id": "gateway",
"version": {
"index": 418
},
"createdAt": "2016-06-16T21:57:11.622222327Z",
"updatedAt": "2016-06-16T21:57:11.622222327Z",
"spec": {
"name": "gateway",
"taskTemplate": {
"containerSpec": {
"image": "alpine",
"args": [
"ping",
"docker.com"
]
},
"resources": {
"limits": {},
"reservations": {}
},
"restartPolicy": {
"condition": "any",
"maxAttempts": 0
},
"placement": {}
},
"mode": {
"replicated": {
"replicas": 2
}
},
"updateConfig": {
"parallelism": 1
},
"endpointSpec": {
"mode": "vip"
}
},
"endpoint": {
"spec": {}
}
},
{
"id": "monitoring",
"version": {
"index": 418
},
"createdAt": "2016-06-16T21:57:11.622222327Z",
"updatedAt": "2016-06-16T21:57:11.622222327Z",
"spec": {
"name": "monitoring",
"taskTemplate": {
"containerSpec": {
"image": "alpine",
"args": [
"ping",
"docker.com"
]
},
"resources": {
"limits": {},
"reservations": {}
},
"restartPolicy": {
"condition": "any",
"maxAttempts": 0
},
"placement": {}
},
"mode": {
"replicated": {
"replicas": 1
}
},
"updateConfig": {
"parallelism": 1
},
"endpointSpec": {
"mode": "vip"
}
},
"endpoint": {
"spec": {}
}
}
],
"services-scale-up": [],
"services-scale-down": []
}

18
ui/api/server.js Normal file
View File

@ -0,0 +1,18 @@
const jsonServer = require('json-server')
const database = require('./db.json')
const server = jsonServer.create()
const router = jsonServer.router(database)
const middleware = jsonServer.defaults()
const rewriter = jsonServer.rewriter({
'/handle/:autoscaler_name/:service_name/inspect-service': '/services-inspect/:service_name',
'/handle/:autoscaler_name/:service_name/up': '/services-scale-up',
'/handle/:autoscaler_name/:service_name/down': '/services-scale-down'
})
server.use(rewriter)
server.use(middleware)
server.use(router)
server.listen(3001, () => {
console.log('JSON Server is running...', '\n')
})

60
ui/components/layout.js Normal file
View File

@ -0,0 +1,60 @@
import Link from 'next/link'
import Head from 'next/head'
import { Container, Grid, Header, List, Menu, Segment } from 'semantic-ui-react'
export default ({ children, title }) => (
<div>
<Head>
<title>Orbiter UI - { title }</title>
<link rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.12/semantic.min.css' />
</Head>
<Segment inverted textAlign='center' vertical>
<Container>
<Menu fixed='top' inverted>
<Link href='/'>
<a className='header item'>
<img className='ui mini image' src='/static/logo-round.svg' style={{ marginRight: '1.5em' }} /> Orbiter UI
</a>
</Link>
</Menu>
</Container>
</Segment>
<Segment vertical style={{ padding: '5em 0em' }}>
<Container>
<Header as='h1' dividing>{ title }</Header>
{ children }
</Container>
</Segment>
<Segment inverted vertical style={{ padding: '3em 0em' }}>
<Container>
<Grid divided inverted stackable>
<Grid.Row>
<Grid.Column width={4}>
<Header inverted as='h4' content='About' />
<List link inverted>
<List.Item as='a'>Link 1</List.Item>
<List.Item as='a'>Link 2</List.Item>
<List.Item as='a'>Link 3</List.Item>
<List.Item as='a'>Link 4</List.Item>
</List>
</Grid.Column>
<Grid.Column width={4}>
<Header inverted as='h4' content='Services' />
<List link inverted>
<List.Item as='a'>Link 1</List.Item>
<List.Item as='a'>Link 2</List.Item>
<List.Item as='a'>Link 3</List.Item>
<List.Item as='a'>Link 4</List.Item>
</List>
</Grid.Column>
<Grid.Column width={8}>
<Header as='h4' inverted>Footer Header</Header>
<p>Description</p>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
</Segment>
</div>
)

1648
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,12 @@
"description": "UI for orbiter autoscaler",
"main": "index.js",
"scripts": {
"dev": "next",
"dev": "next & node api/server.js",
"build": "next build",
"start": "next start",
"test": "standard && npm list 1>/dev/null",
"precommit": "npm test",
"prepush": "npm test"
"lint": "standard",
"lint:fix": "standard --fix",
"test": "npm list 1>/dev/null"
},
"repository": {
"type": "git",
@ -22,13 +22,16 @@
},
"homepage": "https://github.com/gianarb/orbiter/ui",
"dependencies": {
"babel-plugin-inline-dotenv": "^1.1.1",
"babel-plugin-transform-inline-environment-variables": "^0.2.0",
"isomorphic-fetch": "^2.2.1",
"next": "^3.0.6",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"semantic-ui-react": "^0.72.0"
},
"devDependencies": {
"husky": "^0.14.3",
"json-server": "^0.12.0",
"standard": "^10.0.3"
}
}

View File

@ -1,29 +1,86 @@
import Head from 'next/head'
import React from 'react'
import Link from 'next/link'
import { Container, Header, Menu, Segment } from 'semantic-ui-react'
import 'isomorphic-fetch' /* global fetch */
export default () => (
<div>
<Head>
<link rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.12/semantic.min.css' />
</Head>
<Menu fixed='top' inverted>
<Container>
<Link href='/'>
<a className='header item'>
<img className='ui mini image' src='/static/logo-round.svg' style={{ marginRight: '1.5em' }} /> Orbiter UI
</a>
import {Button, Grid, Header, Segment} from 'semantic-ui-react'
import Layout from '../components/layout'
const MIN_REPLICAS = 1
export default class extends React.Component {
static async getInitialProps () {
const res = await fetch(`${process.env.API_HOST}/autoscaler`)
const json = await res.json()
return {
services: json.data
}
}
constructor (props) {
super(props)
this.state = {
services: props.services
}
}
scaleUp (serviceName) {
fetch(`${process.env.API_HOST}/handle/${serviceName}/up`, {
method: 'POST',
body: {} // just pass the instance
}).then(response => {
if (response.status === 201) {
// TODO: update react state manipulation with a better approach
const service = this.state.services.find(item => item.name === serviceName)
const others = this.state.services.filter(item => item.name !== serviceName)
this.setState({ services: [...others, { name: service.name, replicas: service.replicas + 1 }] })
}
})
}
scaleDown (serviceName) {
fetch(`${process.env.API_HOST}/handle/${serviceName}/down`, {
method: 'POST',
body: {} // just pass the instance
}).then(response => {
if (response.status === 201) {
// TODO: update react state manipulation with a better approach
const service = this.state.services.find(item => item.name === serviceName)
const others = this.state.services.filter(item => item.name !== serviceName)
this.setState({ services: [...others, { name: service.name, replicas: service.replicas - 1 }] })
}
})
}
render () {
return (
<Layout title='Dashboard'>
{
this.state.services
.sort((a, b) => a.name.localeCompare(b.name))
.map(service => (
<Segment key={service.name}>
<Grid>
<Grid.Column width={8} style={{ display: 'flex', alignItems: 'center' }}>
<Header as='h3'>
<Link href={{ pathname: '/service', query: { name: service.name } }}>
<a>{ service.name }</a>
</Link>
</Container>
</Menu>
<Container style={{ marginTop: '6em' }}>
<Header as='h1' dividing>Dashboard</Header>
<Segment>Service 1</Segment>
<Segment>Service 2</Segment>
<Segment>Service 3</Segment>
<Segment>Service 4</Segment>
<Segment>Service 5</Segment>
</Container>
</div>
</Header>
</Grid.Column>
<Grid.Column width={4} style={{ display: 'flex', alignItems: 'center' }}>
<Header as='h3'>{ service.replicas }</Header>
</Grid.Column>
<Grid.Column floated='right' width={4} style={{ textAlign: 'right' }}>
<Button icon='plus' onClick={() => { this.scaleUp(service.name) }} />
<Button icon='minus' disabled={service.replicas <= MIN_REPLICAS} onClick={() => { this.scaleDown(service.name) }} />
</Grid.Column>
</Grid>
</Segment>
))
}
</Layout>
)
}
}

27
ui/pages/service.js Normal file
View File

@ -0,0 +1,27 @@
import React from 'react'
import 'isomorphic-fetch' /* global fetch */
import {Segment} from 'semantic-ui-react'
import Layout from '../components/layout'
export default class extends React.Component {
static async getInitialProps ({ query }) {
const res = await fetch(`${process.env.API_HOST}/handle/${query.name}/inspect-service`)
const json = await res.json()
return {
service: json
}
}
render () {
return (
<Layout title='Service info'>
<Segment>
<pre style={{ margin: '0' }}>{ JSON.stringify(this.props.service, null, ' ') }</pre>
</Segment>
</Layout>
)
}
}