mirror of
https://github.com/spaytac/orbiter.git
synced 2026-01-21 21:54:50 +00:00
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:
parent
ed609dc81f
commit
e9f895bb47
13
ui/.babelrc
Normal file
13
ui/.babelrc
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"next/babel",
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"development": {
|
||||||
|
"plugins": ["inline-dotenv"]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"plugins": ["transform-inline-environment-variables"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,11 @@
|
|||||||
# Orbiter UI
|
# Orbiter UI
|
||||||
|
|
||||||
User interface built with React + Next.js
|
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
205
ui/api/db.json
Normal 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
18
ui/api/server.js
Normal 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
60
ui/components/layout.js
Normal 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>
|
||||||
|
)
|
||||||
1646
ui/package-lock.json
generated
1646
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,12 +4,12 @@
|
|||||||
"description": "UI for orbiter autoscaler",
|
"description": "UI for orbiter autoscaler",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next",
|
"dev": "next & node api/server.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"test": "standard && npm list 1>/dev/null",
|
"lint": "standard",
|
||||||
"precommit": "npm test",
|
"lint:fix": "standard --fix",
|
||||||
"prepush": "npm test"
|
"test": "npm list 1>/dev/null"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -22,13 +22,16 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/gianarb/orbiter/ui",
|
"homepage": "https://github.com/gianarb/orbiter/ui",
|
||||||
"dependencies": {
|
"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",
|
"next": "^3.0.6",
|
||||||
"react": "^15.6.1",
|
"react": "^15.6.1",
|
||||||
"react-dom": "^15.6.1",
|
"react-dom": "^15.6.1",
|
||||||
"semantic-ui-react": "^0.72.0"
|
"semantic-ui-react": "^0.72.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^0.14.3",
|
"json-server": "^0.12.0",
|
||||||
"standard": "^10.0.3"
|
"standard": "^10.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,86 @@
|
|||||||
import Head from 'next/head'
|
import React from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { Container, Header, Menu, Segment } from 'semantic-ui-react'
|
import 'isomorphic-fetch' /* global fetch */
|
||||||
|
|
||||||
export default () => (
|
import {Button, Grid, Header, Segment} from 'semantic-ui-react'
|
||||||
<div>
|
|
||||||
<Head>
|
import Layout from '../components/layout'
|
||||||
<link rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.12/semantic.min.css' />
|
|
||||||
</Head>
|
const MIN_REPLICAS = 1
|
||||||
<Menu fixed='top' inverted>
|
|
||||||
<Container>
|
export default class extends React.Component {
|
||||||
<Link href='/'>
|
static async getInitialProps () {
|
||||||
<a className='header item'>
|
const res = await fetch(`${process.env.API_HOST}/autoscaler`)
|
||||||
<img className='ui mini image' src='/static/logo-round.svg' style={{ marginRight: '1.5em' }} /> Orbiter UI
|
const json = await res.json()
|
||||||
</a>
|
return {
|
||||||
</Link>
|
services: json.data
|
||||||
</Container>
|
}
|
||||||
</Menu>
|
}
|
||||||
<Container style={{ marginTop: '6em' }}>
|
|
||||||
<Header as='h1' dividing>Dashboard</Header>
|
constructor (props) {
|
||||||
<Segment>Service 1</Segment>
|
super(props)
|
||||||
<Segment>Service 2</Segment>
|
this.state = {
|
||||||
<Segment>Service 3</Segment>
|
services: props.services
|
||||||
<Segment>Service 4</Segment>
|
}
|
||||||
<Segment>Service 5</Segment>
|
}
|
||||||
</Container>
|
|
||||||
</div>
|
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>
|
||||||
|
</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
27
ui/pages/service.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user