chore: remove copilot packages (#680)

This commit is contained in:
Sérgio Ramos 2017-09-14 16:49:41 +01:00 committed by Wyatt Preul
parent 1fb157240c
commit e842f73b41
326 changed files with 2 additions and 194981 deletions

View File

@ -1,20 +0,0 @@
<a name="1.0.0"></a>
# 1.0.0 (2017-05-25)
### Bug Fixes
* **cp-frontend:** gracefully handle multiple postinstall executions ([73899b2](https://github.com/yldio/joyent-portal/commit/73899b2))
* **cp-frontend:** use `postinstall` hook to patch react-scripts ([d2ac10a](https://github.com/yldio/joyent-portal/commit/d2ac10a))
* **styled-is:** correct package entrypoints ([44a2f2e](https://github.com/yldio/joyent-portal/commit/44a2f2e))
* **ui-toolkit:** compile on postinstall ([7bf95fd](https://github.com/yldio/joyent-portal/commit/7bf95fd))
* **ui-toolkit:** copy fonts before compiling ([19f3678](https://github.com/yldio/joyent-portal/commit/19f3678))
### Features
* **cp-frontend:** Move portal query to header, display result, return user from mock server ([5ffa07a](https://github.com/yldio/joyent-portal/commit/5ffa07a))
* **cp-frontend,ui-toolkit:** style inheritance using `.extend` (#458) ([f3e531d](https://github.com/yldio/joyent-portal/commit/f3e531d))

View File

@ -1,138 +0,0 @@
# Getting Started
## Setup the project
**Install Node.js**, preferably 8.0:
```
λ brew install node
```
with [n](https://github.com/tj/n):
```
λ n 8.0.0
```
**Install Yarn**:
```
λ npm install -g yarn
```
**Install ZMQ**:
```
λ brew install zmq
```
**Clone repo**:
```
λ git clone git@github.com:yldio/joyent-portal.git
Cloning into 'joyent-portal'...
remote: Counting objects: 13702, done.
remote: Compressing objects: 100% (146/146), done.
remote: Total 13702 (delta 89), reused 138 (delta 53), pack-reused 13491
Receiving objects: 100% (13702/13702), 15.08 MiB | 5.44 MiB/s, done.
Resolving deltas: 100% (8824/8824), done.
Downloading legacy/design/ui-library.sketch (8.48 MB)
Checking out files: 100% (1795/1795), done.
λ cd joyent-portal
```
**Install dependendencies**:
```
joyent-portal:master λ yarn
yarn install v0.24.6
[1/5] 🔍 Resolving packages...
[2/5] 🚚 Fetching packages...
[3/5] 🔗 Linking dependencies...
[4/5] 📃 Building fresh packages...
[5/5] ♻️ Cleaning modules...
$ redrun -s clean bootstrap
> lerna clean --yes && lerna bootstrap
lerna info version 2.0.0-rc.5
lerna info versioning independent
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/babel-preset/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/cloudapi-gql/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/cp-frontend/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/cp-gql-mock-server/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/cp-gql-schema/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/cp-rdb-bootstrap/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/docker-compose-client/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/eslint-config/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/normalized-styled-components/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/portal-api/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/portal-data/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/pseudo-json-ast/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/pseudo-yaml-ast/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/remcalc/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/rnd-id/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/styled-is/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/ui-toolkit/node_modules
lerna info clean removing /Users/ramitos/dev/yld/joyent-portal/packages/unitcalc/node_modules
lerna success clean finished
lerna info version 2.0.0-rc.5
lerna info versioning independent
lerna info Bootstrapping 18 packages
lerna info lifecycle preinstall
lerna info Installing external dependencies
lerna info Symlinking packages and binaries
lerna info lifecycle postinstall
lerna info lifecycle prepublish
lerna success Bootstrapped 18 packages
✨ Done in 297.44s.
```
## Start dev environment
**Start mock server**:
```
joyent-portal:master λ cd packages/cp-gql-mock-server
cp-gql-mock-server:master* λ npm run start
> joyent-cp-gql-mock-server@1.0.4 start /Users/ramitos/dev/yld/joyent-portal/packages/cp-gql-mock-server
> node src/index.js
server started at http://0.0.0.0:3000
```
**Start UI Toolkit**:
```
joyent-portal:master* λ cd packages/ui-toolkit
ui-toolkit:master* λ npm run watch
> joyent-ui-toolkit@1.1.0 watch /Users/ramitos/dev/yld/joyent-portal/packages/ui-toolkit
> cross-env NODE_ENV=development redrun -s -c copy-fonts "compile --watch"
> rm -rf dist; mkdir -p dist/typography; cp -r src/typography/libre-franklin dist/typography || true && babel src --out-dir dist --source-maps inline --watch || true
src/anchor/index.js -> dist/anchor/index.js
...
```
**Start Frontend**:
```
joyent-portal:master* λ cd packages/cp-frontend
cp-frontend:master* λ npm run start
> joyent-cp-frontend@1.1.0 start /Users/ramitos/dev/yld/joyent-portal/packages/cp-frontend
> PORT=3069 react-scripts start
Starting the development server...
Compiled successfully!
You can now view joyent-cp-frontend in the browser.
Local: http://localhost:3069/
On Your Network: http://192.168.1.13:3069/
Note that the development build is not optimized.
To create a production build, use yarn run build.
```

View File

@ -1,6 +1,3 @@
![CoPilot Logo](./copilot.png)
[![CircleCI](https://img.shields.io/circleci/project/github/yldio/joyent-portal/master.svg)](https://circleci.com/gh/yldio/joyent-portal) [![CircleCI](https://img.shields.io/circleci/project/github/yldio/joyent-portal/master.svg)](https://circleci.com/gh/yldio/joyent-portal)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme)
@ -21,64 +18,6 @@
## Install ## Install
### Set local environment variables
There is a [`setup.sh`](./setup.sh) script that is used to create an environment (`_env`) file that will contain the keys you use to connect to Triton as well as the keys used to secure the CoPilot installation. In order for this to work correctly you will need to first load the Triton environment variables with the `triton profile` you plan to use. Below is an example of setting these environment variables using the `triton` CLI.
```sh
$ eval "$(triton env)"
```
Additionally, you will need a Certificate Authority certificate file, a server certificate, and a server key file. In the subsection below is an example of generating these files.
### Generating Certificates to Secure CoPilot
To help simplify the creation of certificates there is a _gen-keys.sh_ script. Run it and answer the prompts to generate all of the required keys to secure CoPilot.
```sh
$ ./gen-keys.sh
```
After the client certificate is installed, you may need to restart your browser.
### Generate `_env` file from _setup.sh_
Execute the _setup.sh_ script with the path to your key files.
```sh
$ ./setup.sh ~/path/to/TRITON_PRIVATE_KEY keys-test.com/ca.crt keys-test.com/server.key keys-test.com/server.crt
```
## Usage
You have 3 options for where to run CoPilot. You can either run it using the published docker images locally, or on Triton. The last option is to build the docker images and run docker containers from these locally built images.
### Start CoPilot using published docker images locally
```sh
$ docker-compose up -d
```
Navigate to [https://localhost]() to load the dashboard.
### Deploy and run CoPilot on Triton
```sh
$ docker-compose -f triton-compose.yml up -d
```
Optionally use [_triton-docker_](https://github.com/joyent/triton-docker-cli)
```sh
$ triton-compose -f triton-compose.yml up -d
```
### Build and run CoPilot locally
```sh
$ docker-compose -f local-compose.yml up -d
```
## Contribute ## Contribute
See [the contribute file](CONTRIBUTING.md)! See [the contribute file](CONTRIBUTING.md)!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,114 +0,0 @@
#############################################################################
# CONSUL
#
# Consul is the service catalog that helps discovery between the components
# Change "-bootstrap" to "-bootstrap-expect 3", then scale to 3 or more to
# turn this into an HA Consul raft.
#############################################################################
consul:
image: autopilotpattern/consul:0.7.2-r0.8
command: >
/usr/local/bin/containerpilot
/bin/consul agent -server
-config-dir=/etc/consul
-log-level=err
-bootstrap-expect 1
-ui-dir /ui
restart: always
mem_limit: 128m
ports:
- 8500:8500
dns:
- 127.0.0.1
#############################################################################
# PROMETHEUS
#
# Prometheus is an open source performance monitoring tool
# it is included here for demo purposes and is not required
#############################################################################
prometheus:
image: autopilotpattern/prometheus:1.7.1-r20
restart: always
mem_limit: 1g
ports:
- 9090:9090
links:
- consul:consul
environment:
- CONSUL=consul
- CONSUL_AGENT=1
dns:
- 127.0.0.1
#############################################################################
# FRONTEND
#############################################################################
frontend:
image: joyent/copilot-frontend:1.0.0
mem_limit: 512m
links:
- consul:consul
env_file:
- _env
environment:
- CONSUL=consul
- PORT=443
ports:
- "80:80"
- "443:443"
dns:
- 127.0.0.1
#############################################################################
# BACKEND
#############################################################################
api:
image: joyent/copilot-api:1.8.8
mem_limit: 512m
links:
- consul:consul
- rethinkdb:rethinkdb
env_file:
- _env
environment:
- CONSUL=consul
- PORT=3000
- RETHINK_HOST=rethinkdb
expose:
- 3000
# Docker-compose wrapper
# Create _env file from running ./setup.sh
compose-api:
image: joyent/copilot-compose:1.0.0
links:
- consul:consul
expose:
- 4242
env_file:
- _env
environment:
- CONSUL=consul
restart: always
rethinkdb:
image: autopilotpattern/rethinkdb:2.3.5r1
restart: always
mem_limit: 1g
links:
- consul:consul
env_file:
- _env
environment:
- CONSUL=consul
- CONSUL_AGENT=1
ports:
- 8080:8080
expose:
- 28015
- 29015
dns:
- 127.0.0.1

View File

@ -1,46 +0,0 @@
FROM node:8-alpine
# Install dependencies
RUN set -x \
&& apk update \
&& apk add --update curl bash build-base python zeromq-dev openssh \
&& apk upgrade \
&& rm -rf /var/cache/apk/*
# Install Consul agent
ENV CONSUL_VERSION 0.7.0
ENV CONSUL_CHECKSUM b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8
RUN curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
&& echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
&& unzip /tmp/consul -d /usr/local/bin \
&& rm /tmp/consul.zip \
&& mkdir /config
# Install Containerpilot
ENV CONTAINERPILOT_VERSION 3.4.2
RUN export CONTAINERPILOT_CHECKSUM=5c99ae9ede01e8fcb9b027b5b3cb0cfd8c0b8b88 \
&& export archive=containerpilot-${CONTAINERPILOT_VERSION}.tar.gz \
&& curl -Lso /tmp/${archive} \
"https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/${archive}" \
&& echo "${CONTAINERPILOT_CHECKSUM} /tmp/${archive}" | sha1sum -c \
&& tar zxf /tmp/${archive} -C /bin \
&& rm /tmp/${archive}
# Copy required files
RUN mkdir -p /opt/app/
COPY *.js /opt/app/
COPY package.json /opt/app/
COPY bin /bin
COPY etc /etc
ENV CONTAINERPILOT /etc/containerpilot.json5
# Install dependencies
WORKDIR /opt/app/
ENV BUILD=production
ENV NODE_ENV=production
RUN npm install
CMD ["containerpilot"]

View File

@ -1,21 +0,0 @@
#!/bin/bash
# Copy creds from env vars to files on disk
if [ -n ${!TRITON_CREDS_PATH} ] \
&& [ -n ${!TRITON_CA} ] \
&& [ -n ${!TRITON_CERT} ] \
&& [ -n ${!TRITON_KEY} ]
then
mkdir -p ${TRITON_CREDS_PATH}
echo -e "${TRITON_CA}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/ca.pem
echo -e "${TRITON_CERT}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/cert.pem
echo -e "${TRITON_KEY}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/key.pem
fi
eval `/usr/bin/ssh-agent -s`
mkdir -p ~/.ssh
echo -e "${SDC_KEY_PUB}" | tr '#' '\n' > ~/.ssh/id_rsa.pub
echo -e "${SDC_KEY}" | tr '#' '\n' > ~/.ssh/id_rsa
chmod 400 ~/.ssh/id_rsa.pub
chmod 400 ~/.ssh/id_rsa
ssh-add ~/.ssh/id_rsa

View File

@ -1,44 +0,0 @@
#!/bin/bash
set -e
help() {
echo 'Uses cli tools free and top to determine current CPU and memory usage'
echo 'for the telemetry service.'
}
# memory usage in percent
memory() {
# awk oneliner to get memory usage
# free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }'
# output:
# Memory Usage: 15804/15959MB (99.03%)
local memory=$(free -m | awk 'NR==2{printf "%.2f", $3*100/$2 }')
/bin/containerpilot -putmetric "api_memory_percent=$memory"
}
# cpu load
cpu() {
# oneliner to display cpu load
# top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}'
local cpuload=$(uptime | awk '{printf "%.2f", $6}')
/bin/containerpilot -putmetric "api_cpu_load=$cpuload"
}
diskusage() {
local usage=$(df -P | grep '/$' | awk 'NR=2{print $3}' | sed 's/[^0-9\.]*//g')
/bin/containerpilot -putmetric "api_disk_usage=$usage"
}
diskcapacity() {
local capacity=$(df -P | grep '/$' | awk 'NR=2{print $2}' | sed 's/[^0-9\.]*//g')
/bin/containerpilot -putmetric "api_disk_capacity=$capacity"
}
cmd=$1
if [ ! -z "$cmd" ]; then
shift 1
$cmd "$@"
exit
fi
help

View File

@ -1,142 +0,0 @@
'use strict';
const Data = require('portal-api/lib/data');
const Fs = require('fs');
const Path = require('path');
const Piloted = require('piloted');
const Triton = require('triton');
const Url = require('url');
let timeoutId;
const loadConfig = function () {
const docker = Piloted.service('docker-compose-api');
const rethink = Piloted.service('rethinkdb');
const retry = () => {
timeoutId = setTimeout(() => {
timeoutId = null;
Piloted.refresh();
}, 1000);
};
if (docker && rethink) {
bootstrap({ docker, rethink }, (err) => {
if (err) {
console.error(err);
return retry();
}
process.exit(0);
});
} else if (!timeoutId) {
retry();
}
};
Piloted.on('refresh', () => {
loadConfig();
});
const bootstrap = function ({ docker, rethink }, cb) {
const settings = {
db: {
host: rethink.address
},
docker: {
protocol: 'https',
host: docker.address,
port: docker.port,
ca: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'ca.pem'))
: undefined,
cert: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'cert.pem'))
: undefined,
key: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'key.pem'))
: undefined
},
triton: {
url: process.env.SDC_URL,
account: process.env.SDC_ACCOUNT,
keyId: process.env.SDC_KEY_ID
}
};
const data = new Data(settings);
const region = process.env.TRITON_DC || 'us-sw-1';
data.connect((err) => {
if (err) {
return cb(err);
}
data.getDatacenters((err, datacenters) => {
if (err) {
return cb(err);
}
// Don't continue since data is already bootstrapped
if (datacenters && datacenters.length) {
return cb();
}
data.createDatacenter({
region,
name: region
}, (err, datacenter) => {
if (err) {
return cb(err);
}
Triton.createClient({
profile: settings.triton
}, (err, { cloudapi }) => {
if (err) {
return cb(err);
}
cloudapi.getAccount((err, {
id,
firstName,
lastName,
email,
login
}) => {
if (err) {
return cb(err);
}
data.createUser({
tritonId: id,
firstName,
lastName,
email,
login
}, (err, user) => {
if (err) {
return cb(err);
}
data.createPortal({
user,
datacenter
}, (err, portal) => {
if (err) {
return cb(err);
}
console.log('data bootstrapped');
cb();
});
});
});
});
});
});
});
};
loadConfig();

View File

@ -1,168 +0,0 @@
{
consul: 'localhost:8500',
jobs: [
{
name: 'setup-config',
exec: '/bin/prestart.sh'
},
{
name: 'bootstrap',
exec: 'node bootstrap-data.js',
when: {
source: 'setup-config',
once: 'exitSuccess'
}
},
{
name: 'api',
port: {{.PORT}},
exec: 'node server.js',
health: {
exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:{{.PORT}}/check-it-out',
interval: 5,
ttl: 5
},
when: {
source: 'bootstrap',
once: 'exitSuccess'
},
restarts: 'unlimited'
},
{
name: 'consul-agent',
exec: ['/usr/local/bin/consul', 'agent',
'-data-dir=/data',
'-config-dir=/config',
'-log-level=err',
'-rejoin',
'-retry-join', '{{ .CONSUL | default "consul" }}',
'-retry-max', '20',
'-retry-interval', '5s'],
restarts: 'unlimited'
},
{
name: 'sensor_memory_usage',
exec: '/bin/sensors.sh memory',
timeout: '5s',
when: {
interval: '5s'
},
restarts: 'unlimited'
},
{
name: 'sensor_cpu_load',
exec: '/bin/sensors.sh cpu',
timeout: '5s',
when: {
interval: '5s'
},
restarts: 'unlimited'
},
{
name: 'sensor_disk_capacity',
exec: '/bin/sensors.sh diskcapacity',
timeout: '5s',
when: {
interval: '60s'
},
restarts: 'unlimited'
},
{
name: 'sensor_disk_usage',
exec: '/bin/sensors.sh diskusage',
timeout: '5s',
when: {
interval: '60s'
},
restarts: 'unlimited'
},
{
name: 'onchange-compose-api',
exec: 'pkill -SIGHUP node',
when: {
source: 'watch.docker-compose-api',
each: 'changed'
}
},
{
name: 'onchange-rethinkdb',
exec: 'pkill -SIGHUP node',
when: {
source: 'watch.rethinkdb',
each: 'changed'
}
}
],
watches: [
{
name: 'docker-compose-api',
interval: 3
},
{
name: 'rethinkdb',
interval: 3
}
],
telemetry: {
port: 9090,
tags: ['op'],
metrics: [
{
namespace: 'api',
subsystem: 'memory',
name: 'percent',
help: 'Percentage of memory used',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'cpu',
name: 'load',
help: 'CPU load',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'disk',
name: 'capacity',
help: 'Disk capacity',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'disk',
name: 'usage',
help: 'Disk usage',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'request',
name: 'concurrent',
help: 'Number of concurrent requests',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'process',
name: 'up_time',
help: 'Process up time',
type: 'counter'
},
{
namespace: 'api',
subsystem: 'process',
name: 'mem_rss',
help: 'Process memory RSS usage',
type: 'gauge'
},
{
namespace: 'api',
subsystem: 'process',
name: 'heap_used',
help: 'Process heap usage',
type: 'gauge'
}
]
}
}

View File

@ -1,28 +0,0 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "./server.js",
"scripts": {
"start": "node server.js"
},
"keywords": [],
"author": "wyatt",
"license": "MPL-2.0",
"dependencies": {
"boom": "^5.1.0",
"brule": "^2.0.0",
"good": "^7.2.0",
"good-console": "^6.4.0",
"good-squeeze": "^5.0.2",
"graphi": "^2.3.0",
"hapi": "^16.6.0",
"hoek": "^4.1.1",
"joi": "^10.6.0",
"joyent-cp-gql-schema": "^1.7.0",
"piloted": "^3.1.1",
"portal-api": "^1.8.8",
"toppsy": "^1.1.0",
"triton": "^5.2.0"
}
}

View File

@ -1,137 +0,0 @@
'use strict';
const Brule = require('brule');
const Fs = require('fs');
const Good = require('good');
const Hapi = require('hapi');
const Path = require('path');
const Piloted = require('piloted');
const Portal = require('portal-api');
const Toppsy = require('toppsy');
const Url = require('url');
let started = false;
let timeoutId;
const loadConfig = function () {
const docker = Piloted.service('docker-compose-api');
const rethink = Piloted.service('rethinkdb');
if (docker && rethink && !started) {
started = true;
startServer({ docker, rethink });
} else if (!started && !timeoutId) {
timeoutId = setTimeout(() => {
timeoutId = null;
Piloted.refresh();
}, 1000);
}
};
Piloted.on('refresh', () => {
loadConfig();
});
const startServer = function ({ docker, rethink }) {
const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
const server = new Hapi.Server();
server.connection({ port });
const portalOptions = {
data: {
db: {
host: rethink.address
},
docker: {
protocol: 'https',
host: docker.address,
port: docker.port,
ca: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'ca.pem'))
: undefined,
cert: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'cert.pem'))
: undefined,
key: process.env.DOCKER_CERT_PATH
? Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'key.pem'))
: undefined
},
triton: {
url: process.env.SDC_URL,
account: process.env.SDC_ACCOUNT,
keyId: process.env.SDC_KEY_ID
}
},
watch: {
url: process.env.SDC_URL,
account: process.env.SDC_ACCOUNT,
keyId: process.env.SDC_KEY_ID
}
};
const goodOptions = {
ops: {
interval: 1000
},
reporters: {
consoleReporter: [
{
module: 'good-squeeze',
name: 'Squeeze',
args: [{ response: '*', error: '*' }]
},
{
module: 'good-console'
},
'stdout'
]
}
};
server.register(
[
Brule,
{
register: Good,
options: goodOptions
},
{
register: Portal,
options: portalOptions
},
{
register: Toppsy,
options: { namespace: 'portal', subsystem: 'api' }
}
],
(err) => {
handlerError(err);
server.start((err) => {
handlerError(err);
console.log(`server started at http://localhost:${server.info.port}`);
});
}
);
};
const handlerError = function (err) {
if (err) {
console.error(err);
console.error(err.stack);
process.exit(1);
}
};
process.on('uncaughtException', (err) => {
console.error(err);
console.error(err.stack);
});
process.on('unhandledRejection', (err) => {
console.error(err);
console.error(err.stack);
});
loadConfig();

View File

@ -1,29 +0,0 @@
FROM ramitos/docker-compose-api:1.0.0
RUN apk add --update bash
RUN export CONSUL_VERSION=0.7.0 \
&& export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \
&& curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
&& echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
&& unzip /tmp/consul -d /usr/local/bin \
&& rm /tmp/consul.zip \
&& mkdir /config
# Install Containerpilot
ENV CONTAINERPILOT_VERSION 3.4.1
RUN export CONTAINERPILOT_CHECKSUM=4d13cfb345de86135ab2271b77516c6b6a7bed3a \
&& export archive=containerpilot-${CONTAINERPILOT_VERSION}.tar.gz \
&& curl -Lso /tmp/${archive} \
"https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/${archive}" \
&& echo "${CONTAINERPILOT_CHECKSUM} /tmp/${archive}" | sha1sum -c \
&& tar zxf /tmp/${archive} -C /bin \
&& rm /tmp/${archive}
# Add Containerpilot configuration
COPY etc/containerpilot.json /etc
ENV CONTAINERPILOT /etc/containerpilot.json
COPY bin /bin
ENTRYPOINT []
CMD ["containerpilot"]

View File

@ -1,13 +0,0 @@
#!/bin/bash
# Copy creds from env vars to files on disk
if [ -n ${!TRITON_CREDS_PATH} ] \
&& [ -n ${!TRITON_CA} ] \
&& [ -n ${!TRITON_CERT} ] \
&& [ -n ${!TRITON_KEY} ]
then
mkdir -p ${TRITON_CREDS_PATH}
echo -e "${TRITON_CA}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/ca.pem
echo -e "${TRITON_CERT}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/cert.pem
echo -e "${TRITON_KEY}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/key.pem
fi

View File

@ -1,45 +0,0 @@
{
"consul": "localhost:8500",
"jobs": [
{
"name": "setup-config",
"exec": "/bin/prestart.sh"
},
{
"name": "docker-compose-api",
"port": 4242,
"exec": [
"python",
"-u",
"./bin/docker-compose"
],
"health": {
"exec": "true",
"interval": 10,
"ttl": 25
},
"when": {
"source": "setup-config",
"once": "exitSuccess"
},
"restarts": "unlimited"
},
{
"name": "consul-agent",
"exec": ["/usr/local/bin/consul", "agent",
"-data-dir=/data",
"-config-dir=/config",
"-log-level=err",
"-rejoin",
"-retry-join", "{{ .CONSUL | default "consul" }}",
"-retry-max", "10",
"-retry-interval", "10s"],
"health": {
"exec": "curl -so /dev/null http://localhost:8500",
"interval": 10,
"ttl": 25
},
"restarts": "unlimited"
}
]
}

View File

@ -1,52 +0,0 @@
FROM node:8-alpine
# Install dependencies
RUN set -x \
&& apk update \
&& apk add --update curl bash nginx openssl \
&& apk upgrade \
&& rm -rf /var/cache/apk/*
# Install Consul template
# Releases at https://releases.hashicorp.com/consul-template/
RUN set -ex \
&& export CONSUL_TEMPLATE_VERSION=0.19.0 \
&& export CONSUL_TEMPLATE_CHECKSUM=31dda6ebc7bd7712598c6ac0337ce8fd8c533229887bd58e825757af879c5f9f \
&& curl --retry 7 --fail -Lso /tmp/consul-template.zip "https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip" \
&& echo "${CONSUL_TEMPLATE_CHECKSUM} /tmp/consul-template.zip" | sha256sum -c \
&& unzip /tmp/consul-template.zip -d /usr/local/bin \
&& rm /tmp/consul-template.zip
# Install Consul agent
ENV CONSUL_VERSION 0.7.0
ENV CONSUL_CHECKSUM b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8
RUN curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \
&& echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \
&& unzip /tmp/consul -d /usr/local/bin \
&& rm /tmp/consul.zip \
&& mkdir /config
# Install Containerpilot
ENV CONTAINERPILOT_VERSION 3.4.1
RUN export CONTAINERPILOT_CHECKSUM=4d13cfb345de86135ab2271b77516c6b6a7bed3a \
&& export archive=containerpilot-${CONTAINERPILOT_VERSION}.tar.gz \
&& curl -Lso /tmp/${archive} \
"https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/${archive}" \
&& echo "${CONTAINERPILOT_CHECKSUM} /tmp/${archive}" | sha1sum -c \
&& tar zxf /tmp/${archive} -C /bin \
&& rm /tmp/${archive}
ENV NODE_ENV=production
# Copy required files
COPY ./bin /bin
COPY ./etc/nginx.conf.tmpl /etc/nginx/nginx.conf.tmpl
COPY ./etc/containerpilot.json5 /etc/containerpilot.json5
ENV CONTAINERPILOT /etc/containerpilot.json5
RUN mkdir -p /opt/app/
WORKDIR /opt/app/
RUN npm pack joyent-cp-frontend
RUN tar -xzf joyent-cp-frontend*.tgz
CMD ["containerpilot"]

View File

@ -1,51 +0,0 @@
#!/bin/bash
# Render Nginx configuration template using values from Consul,
# but do not reload because Nginx has't started yet.
# Install key files for TLS auth in nginx
preStart() {
# Copy creds from env vars to files on disk
if [ -n ${!NGINX_CA_CRT} ] \
&& [ -n ${!NGINX_SERVER_KEY} ] \
&& [ -n ${!NGINX_SERVER_CRT} ]
then
local nginx_path=/etc/nginx/certs
mkdir -p $nginx_path
mkdir -p $nginx_path/ca
mkdir -p $nginx_path/server
echo -e "${NGINX_CA_CRT}" | tr '#' '\n' > $nginx_path/ca/ca.crt
echo -e "${NGINX_SERVER_KEY}" | tr '#' '\n' > $nginx_path/server/server.key
echo -e "${NGINX_SERVER_CRT}" | tr '#' '\n' > $nginx_path/server/server.crt
chmod 444 $nginx_path/ca/ca.crt
chmod 444 $nginx_path/server/server.key
chmod 444 $nginx_path/server/server.crt
fi
consul-template \
-once \
-consul-addr "localhost:8500" \
-template "/etc/nginx/nginx.conf.tmpl:/etc/nginx/nginx.conf"
}
# Render Nginx configuration template using values from Consul,
# then gracefully reload Nginx
onChange() {
consul-template \
-once \
-consul-addr "localhost:8500" \
-template "/etc/nginx/nginx.conf.tmpl:/etc/nginx/nginx.conf:nginx -s reload"
}
until
cmd=$1
if [ -z "$cmd" ]; then
onChange
fi
shift 1
$cmd "$@"
[ "$?" -ne 127 ]
do
onChange
exit
done

View File

@ -1,44 +0,0 @@
#!/bin/bash
set -e
help() {
echo 'Uses cli tools free and top to determine current CPU and memory usage'
echo 'for the telemetry service.'
}
# memory usage in percent
memory() {
# awk oneliner to get memory usage
# free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }'
# output:
# Memory Usage: 15804/15959MB (99.03%)
local memory=$(free -m | awk 'NR==2{printf "%.2f", $3*100/$2 }')
/bin/containerpilot -putmetric "frontend_memory_percent=$memory"
}
# cpu load
cpu() {
# oneliner to display cpu load
# top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}'
local cpuload=$(uptime | awk '{printf "%.2f", $6}')
/bin/containerpilot -putmetric "frontend_cpu_load=$cpuload"
}
diskusage() {
local usage=$(df -P | grep '/$' | awk 'NR=2{print $3}' | sed 's/[^0-9\.]*//g')
/bin/containerpilot -putmetric "frontend_disk_usage=$usage"
}
diskcapacity() {
local capacity=$(df -P | grep '/$' | awk 'NR=2{print $2}' | sed 's/[^0-9\.]*//g')
/bin/containerpilot -putmetric "frontend_disk_capacity=$capacity"
}
cmd=$1
if [ ! -z "$cmd" ]; then
shift 1
$cmd "$@"
exit
fi
help

View File

@ -1,130 +0,0 @@
{
consul: 'localhost:8500',
jobs: [
{
name: 'consul-agent',
exec: ['/usr/local/bin/consul', 'agent',
'-data-dir=/data',
'-config-dir=/config',
'-log-level=err',
'-rejoin',
'-retry-join', '{{ .CONSUL | default "consul" }}',
'-retry-max', '10',
'-retry-interval', '10s'],
health: {
exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:8500',
interval: 5,
ttl: 25
},
restarts: 'unlimited'
},
{
name: "preStart",
exec: "/bin/reload-nginx.sh preStart",
when: {
source: 'consul-agent',
once: 'healthy'
},
},
{
name: 'cp-frontend',
port: {{.PORT}},
exec: 'nginx',
interfaces: ["eth0", "eth1"],
restarts: 'unlimited',
when: {
source: 'preStart',
once: 'exitSuccess'
},
health: {
exec: 'pstree nginx',
interval: 10,
ttl: 25
}
},
{
name: "onchange-api",
exec: "/bin/reload-nginx.sh onChange",
when: {
source: "watch.api",
each: "changed"
}
},
{
name: 'sensor_memory_usage',
exec: '/bin/sensors.sh memory',
timeout: '5s',
when: {
interval: '5s'
},
restarts: 'unlimited'
},
{
name: 'sensor_cpu_load',
exec: '/bin/sensors.sh cpu',
timeout: '5s',
when: {
interval: '5s'
},
restarts: 'unlimited'
},
{
name: 'sensor_disk_capacity',
exec: '/bin/sensors.sh diskcapacity',
timeout: '5s',
when: {
interval: '60s'
},
restarts: 'unlimited'
},
{
name: 'sensor_disk_usage',
exec: '/bin/sensors.sh diskusage',
timeout: '5s',
when: {
interval: '60s'
},
restarts: 'unlimited'
}
],
watches: [
{
name: 'api',
interval: 3
}
],
telemetry: {
port: 9090,
tags: ['op'],
metrics: [
{
namespace: 'frontend',
subsystem: 'memory',
name: 'percent',
help: 'Percentage of memory used',
type: 'gauge'
},
{
namespace: 'frontend',
subsystem: 'cpu',
name: 'load',
help: 'CPU load',
type: 'gauge'
},
{
namespace: 'frontend',
subsystem: 'disk',
name: 'capacity',
help: 'Disk capacity',
type: 'gauge'
},
{
namespace: 'frontend',
subsystem: 'disk',
name: 'usage',
help: 'Disk usage',
type: 'gauge'
}
]
}
}

View File

@ -1,137 +0,0 @@
# /etc/nginx/nginx.conf
user nginx;
worker_processes 1;
daemon off;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
}
http {
index index.html index.htm;
server {
server_name _;
listen 80;
listen [::]:80;
location / {
rewrite ^ https://$host$request_uri? permanent;
}
}
{{ if service "api" }}
upstream api_hosts {
{{range service "api"}}
server {{.Address}}:{{.Port}};
{{end}}
}{{ end }}
server {
listen 443 ssl;
listen [::]:443 ssl;
root /opt/app/package/build;
ssl_certificate /etc/nginx/certs/server/server.crt;
ssl_certificate_key /etc/nginx/certs/server/server.key;
ssl_client_certificate /etc/nginx/certs/ca/ca.crt;
ssl_verify_client on;
ssl_session_timeout 1d;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
location / {
try_files $uri /index.html;
}
{{ if service "api" }}
location /api {
rewrite /api/(.*) /$1 break;
proxy_pass http://api_hosts;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Client-Dn $ssl_client_s_dn;
}{{ end }}
}
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Name servers used to resolve names of upstream servers into addresses.
# It's also needed when using tcpsocket and udpsocket in Lua modules.
#resolver 208.67.222.222 208.67.220.220;
# Don't tell nginx version to clients.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable.
client_max_body_size 1m;
# Timeout for keep-alive connections. Server will close connections after
# this time.
keepalive_timeout 65;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write().
sendfile on;
# Don't buffer data-sends (disable Nagle algorithm).
# Good for sending frequent small bursts of data in real time.
tcp_nodelay on;
# Causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames.
#tcp_nopush on;
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
# Specifies that our cipher suits should be preferred over client ciphers.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
ssl_session_cache shared:SSL:2m;
# Enable gzipping of responses.
#gzip on;
# Set the Vary HTTP header as defined in the RFC 2616.
gzip_vary on;
# Enable checking the existence of precompressed files.
#gzip_static on;
# Specifies the main log format.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /var/log/nginx/access.log main;
# Includes virtual hosts configs.
# include /etc/nginx/conf.d/*.conf;
}

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e -o pipefail
TRITON_ACCOUNT=$(triton account get | awk -F": " '/id:/{print $2}')
TRITON_DC=$(triton profile get | awk -F"/" '/url:/{print $3}' | awk -F'.' '{print $1}')
DEFAULT_DOMAIN=${TRITON_ACCOUNT}.${TRITON_DC}.cns.triton.zone
read -p "Enter the domain name you plan to use for this key [$DEFAULT_DOMAIN]: " domain
domain="${domain:-$DEFAULT_DOMAIN}"
echo -n "Enter the password to use for the key: "
read -s password
echo
echo "Generating key for $domain"
keys_path=keys-$domain
mkdir -p $keys_path
openssl genrsa -aes256 -passout pass:$password -out $keys_path/ca.key 4096
chmod 400 $keys_path/ca.key
openssl req -new -x509 -sha256 -days 730 -key $keys_path/ca.key -out $keys_path/ca.crt -passin pass:$password -subj "/CN=copilot"
chmod 444 $keys_path/ca.crt
openssl genrsa -out $keys_path/server.key 2048
chmod 400 $keys_path/server.key
openssl req -new -key $keys_path/server.key -sha256 -out $keys_path/server.csr -passin pass:$password -subj "/CN=$domain"
openssl x509 -req -days 365 -sha256 -in $keys_path/server.csr -passin pass:$password -CA $keys_path/ca.crt -CAkey $keys_path/ca.key -set_serial 1 -out $keys_path/server.crt
chmod 444 $keys_path/server.crt
openssl genrsa -out $keys_path/client.key 2048
openssl req -new -key $keys_path/client.key -out $keys_path/client.csr -subj "/CN=$domain"
openssl x509 -req -days 365 -sha256 -in $keys_path/client.csr -CA $keys_path/ca.crt -CAkey $keys_path/ca.key -set_serial 2 -out $keys_path/client.crt -passin pass:$password
openssl pkcs12 -export -clcerts -in $keys_path/client.crt -inkey $keys_path/client.key -out $keys_path/client.p12 -passout pass:$password
open $keys_path/client.p12 &
echo
echo "You can complete setup by running './setup.sh ~/path/to/TRITON_PRIVATE_KEY $keys_path/ca.crt $keys_path/server.key $keys_path/server.crt'"

View File

@ -1,106 +0,0 @@
#############################################################################
# CONSUL
#
# Consul is the service catalog that helps discovery between the components
# Change "-bootstrap" to "-bootstrap-expect 3", then scale to 3 or more to
# turn this into an HA Consul raft.
#############################################################################
consul:
image: autopilotpattern/consul:0.7.2-r0.8
command: >
/usr/local/bin/containerpilot
/bin/consul agent -server
-config-dir=/etc/consul
-log-level=err
-bootstrap-expect 1
-ui-dir /ui
restart: always
mem_limit: 128m
ports:
- 8500:8500
#############################################################################
# PROMETHEUS
#
# Prometheus is an open source performance monitoring tool
# it is included here for demo purposes and is not required
#############################################################################
prometheus:
image: autopilotpattern/prometheus:1.7.1-r24
restart: always
mem_limit: 1g
ports:
- 9090:9090
links:
- consul:consul
environment:
- CONSUL=consul
- CONSUL_AGENT=1
#############################################################################
# FRONTEND
#############################################################################
frontend:
build: docker/frontend
mem_limit: 512m
links:
- consul:consul
env_file:
- _env
environment:
- CONSUL=consul
- PORT=443
ports:
- "80:80"
- "443:443"
#############################################################################
# BACKEND
#############################################################################
api:
build: docker/api
mem_limit: 512m
links:
- consul:consul
env_file:
- _env
environment:
- CONSUL=consul
- PORT=3000
expose:
- 3000
# Docker-compose wrapper
# Create _env file from running ./setup.sh
compose-api:
build: docker/compose-api
links:
- consul:consul
expose:
- 4242
env_file:
- _env
environment:
- CONSUL=consul
restart: always
rethinkdb:
image: autopilotpattern/rethinkdb:2.3.5r1
restart: always
mem_limit: 1g
links:
- consul:consul
env_file:
- _env
environment:
- CONSUL=consul
- CONSUL_AGENT=1
ports:
- 8080:8080
expose:
- 28015
- 29015
dns:
- 127.0.0.1

View File

@ -1,5 +1,5 @@
{ {
"name": "container-pilot-dashboard", "name": "joyent-portal",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"license": "MPL-2.0", "license": "MPL-2.0",
@ -14,7 +14,7 @@
"lint-license": "./scripts/license-to-fail", "lint-license": "./scripts/license-to-fail",
"lint-docs": "./scripts/quality-docs", "lint-docs": "./scripts/quality-docs",
"lint-ci:root": "lint-ci:root":
"echo 0 `# eslint scripts/* --format junit --output-file $CIRCLE_TEST_REPORTS/lint/container-pilot-dashboard.xml`", "echo 0 `# eslint scripts/* --format junit --output-file $CIRCLE_TEST_REPORTS/lint/joyent-portal.xml`",
"lint:root": "echo 0 `# eslint scripts/* --fix`", "lint:root": "echo 0 `# eslint scripts/* --fix`",
"lint-ci:packages": "lerna run lint-ci", "lint-ci:packages": "lerna run lint-ci",
"lint:packages": "lerna run lint", "lint:packages": "lerna run lint",

View File

@ -1,9 +0,0 @@
{
"presets": "joyent-portal",
"plugins": [
"styled-components",
["inline-react-svg", {
"ignorePattern": "libre-franklin"
}]
]
}

View File

@ -1,9 +0,0 @@
src/components/base/*.css
node_modules
coverage
.nyc_output
docs/static
!docs/static/index.html
docs/node_modules
dist
package-lock.json

View File

@ -1,4 +0,0 @@
.nyc_output
coverage
dist
build

View File

@ -1,11 +0,0 @@
{
"extends": "joyent-portal",
"rules": {
"no-console": 0,
"new-cap": 0,
// temp
"no-undef": 1,
"no-debugger": 1,
"no-negated-condition": 0
}
}

View File

@ -1,18 +0,0 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,5 +0,0 @@
{
"syntax": "scss",
"processors": ["stylelint-processor-styled-components"],
"extends": ["stylelint-config-standard", "stylelint-config-primer"]
}

View File

@ -1,15 +0,0 @@
{
"libs": [
"ecmascript",
"browser"
],
"plugins": {
"doc_comment": true,
"local-scope": true,
"jsx": true,
"node": true,
"webpack": {
"configPath": "./node_modules/react-scripts/config/webpack.config.dev.js"
}
}
}

View File

@ -1,24 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="1.1.0"></a>
# 1.1.0 (2017-05-25)
### Bug Fixes
* **cp-frontend:** gracefully handle multiple postinstall executions ([73899b2](https://github.com/yldio/joyent-portal/commit/73899b2))
* **cp-frontend:** use `postinstall` hook to patch react-scripts ([d2ac10a](https://github.com/yldio/joyent-portal/commit/d2ac10a))
### Features
* **cp-frontend,ui-toolkit:** style inheritance using `.extend` (#458) ([f3e531d](https://github.com/yldio/joyent-portal/commit/f3e531d))
<a name="1.0.3"></a>
## 1.0.3 (2017-05-25)

View File

@ -1,19 +0,0 @@
FROM quay.io/yldio/alpine-node-containerpilot:latest
RUN apk add --update nginx
ENV CONTAINERPILOT /etc/containerpilot.json5
RUN npm install -g npm@^4
RUN npm config set loglevel info \
&& yarn add lerna@^2.0.0
RUN ./node_modules/.bin/lerna clean --yes --scope joyent-cp-frontend --include-filtered-dependencies \
&& ./node_modules/.bin/lerna bootstrap --scope joyent-cp-frontend --include-filtered-dependencies
COPY packages/cp-frontend/etc/containerpilot.json5 ${CONTAINERPILOT}
COPY packages/cp-frontend/etc/nginx.conf.tmpl /etc/nginx/nginx.conf.tmpl
WORKDIR /opt/app/packages/cp-frontend
CMD ["/bin/containerpilot"]

View File

@ -1,23 +0,0 @@
# joyent-cp-frontend
[![Docker Repository on Quay](https://quay.io/repository/yldio/joyent-cp-frontend/status)](https://quay.io/repository/yldio/joyent-cp-frontend)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme)
[![demo master](https://img.shields.io/badge/demo-master-3B47CC.svg)](http://cp-frontend-master.svc.f4b20699-b323-4452-9091-977895896da6.eu-ams-1.triton.zone:3069)
[![demo staging](https://img.shields.io/badge/demo-staging-3B47CC.svg)](http://cp-frontend-staging.svc.f4b20699-b323-4452-9091-977895896da6.eu-ams-1.triton.zone:3069)
## Table of Contents
- [Usage](#usage)
- [License](#license)
## Usage
```
npm run start
open http://0.0.0.0:3069
```
## License
MPL-2.0

View File

@ -1,54 +0,0 @@
{
consul: 'localhost:8500',
jobs: [
{
name: 'is-built',
exec: '[ -d /opt/app/packages/cp-frontend/build/static ]'
},
{
name: 'build',
exec: 'yarn run build',
when: {
source: 'is-built',
once: 'exitFailed'
}
},
{
name: 'config-nginx',
exec: 'containerpilot -config /etc/nginx/nginx.conf.tmpl -template -out /etc/nginx/nginx.conf'
},
{
name: 'cp-frontend',
port: {{.PORT}},
exec: 'nginx',
interfaces: ["eth0", "eth1"],
restarts: 'unlimited',
when: {
source: 'config-nginx',
once: 'exitSuccess'
},
health: {
exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:{{.PORT}}',
interval: 5,
ttl: 25
},
tags: [
'traefik.backend=cp-frontend',
'traefik.frontend.rule=PathPrefix:/',
'traefik.frontend.entryPoints={{ .ENTRYPOINTS | default "http,ws,wss" }}'
]
},
{
name: 'consul-agent',
exec: ['/usr/local/bin/consul', 'agent',
'-data-dir=/data',
'-config-dir=/config',
'-log-level=err',
'-rejoin',
'-retry-join', '{{ .CONSUL | default "consul" }}',
'-retry-max', '10',
'-retry-interval', '10s'],
restarts: 'unlimited'
}
]
}

View File

@ -1,101 +0,0 @@
# /etc/nginx/nginx.conf
user nginx;
worker_processes 1;
daemon off;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
}
http {
index index.html index.htm;
server {
server_name _;
listen {{ .PORT | default "80" }} default_server;
listen [::]:{{ .PORT | default "80" }} default_server;
root /opt/app/packages/cp-frontend/build;
location / {
try_files $uri /index.html;
}
}
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Name servers used to resolve names of upstream servers into addresses.
# It's also needed when using tcpsocket and udpsocket in Lua modules.
#resolver 208.67.222.222 208.67.220.220;
# Don't tell nginx version to clients.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable.
client_max_body_size 1m;
# Timeout for keep-alive connections. Server will close connections after
# this time.
keepalive_timeout 65;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write().
sendfile on;
# Don't buffer data-sends (disable Nagle algorithm).
# Good for sending frequent small bursts of data in real time.
tcp_nodelay on;
# Causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames.
#tcp_nopush on;
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
# Specifies that our cipher suits should be preferred over client ciphers.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
ssl_session_cache shared:SSL:2m;
# Enable gzipping of responses.
#gzip on;
# Set the Vary HTTP header as defined in the RFC 2616.
gzip_vary on;
# Enable checking the existence of precompressed files.
#gzip_static on;
# Specifies the main log format.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /var/log/nginx/access.log main;
# Includes virtual hosts configs.
# include /etc/nginx/conf.d/*.conf;
}

View File

@ -1,94 +0,0 @@
{
"name": "joyent-cp-frontend",
"version": "1.4.0",
"license": "MPL-2.0",
"repository": "github:yldio/joyent-portal",
"main": "build/",
"scripts": {
"dev": "REACT_APP_GQL_PORT=3000 PORT=3069 REACT_APP_GQL_PROTOCOL=http react-scripts start",
"start": "PORT=3069 react-scripts start",
"build": "NODE_ENV=production react-scripts build",
"lint:css": "echo 0",
"lint:js": "eslint . --fix",
"lint": "redrun -s lint:*",
"lint-ci:css": "echo 0",
"lint-ci:js": "eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/cp-frontend.xml",
"lint-ci": "redrun -p lint-ci:*",
"test": "NODE_ENV=test ./test/run --env=jsdom",
"test-ci": "echo 0 `# NODE_ENV=test JEST_JUNIT_OUTPUT=$CIRCLE_TEST_REPORTS/test/cp-frontend.xml ./test/run --env=jsdom --coverage --coverageDirectory=$CIRCLE_ARTIFACTS/cp-frontend --testResultsProcessor=$(node -e \"console.log(require.resolve('jest-junit'))\")`",
"prepublish": "node scripts/postinstall"
},
"dependencies": {
"apollo": "^0.2.2",
"apr-intercept": "^1.0.4",
"chart.js": "^2.6.0",
"constant-case": "^2.0.0",
"force-array": "^3.1.0",
"graphql-tag": "^2.4.2",
"jest-cli": "^20.0.4",
"joyent-manifest-editor": "^1.1.0",
"joyent-ui-toolkit": "^1.2.0",
"js-yaml": "^3.9.1",
"lodash.find": "^4.6.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.isstring": "^4.0.1",
"lodash.remove": "^4.7.0",
"lodash.uniq": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"normalized-styled-components": "^1.0.9",
"param-case": "^2.1.1",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-apollo": "^1.4.15",
"react-bundle": "^1.0.4",
"react-codemirror": "^1.0.0",
"react-dom": "^15.6.1",
"react-redux": "^5.0.6",
"react-router": "^4.1.1",
"react-router-dom": "^4.1.2",
"react-simple-table": "^1.0.1",
"react-styled-flexboxgrid": "^2.0.3",
"redux": "^3.7.2",
"redux-actions": "^2.2.1",
"redux-batched-actions": "^0.2.0",
"redux-form": "^7.0.3",
"remcalc": "^1.0.8",
"reselect": "^3.0.1",
"simple-statistics": "^4.1.1",
"styled-components": "^2.1.2",
"styled-is": "^1.0.11",
"styled-text-spinners": "^1.0.1",
"title-case": "^2.1.1",
"unitcalc": "^1.0.8",
"uuid": "^3.1.0"
},
"devDependencies": {
"apr-for-each": "^1.0.6",
"apr-main": "^1.0.7",
"babel-plugin-inline-react-svg": "^0.4.0",
"babel-plugin-styled-components": "^1.2.0",
"babel-preset-joyent-portal": "^2.0.0",
"cross-env": "^5.0.5",
"eslint": "^4.5.0",
"eslint-config-joyent-portal": "3.0.0",
"jest": "^21.0.1",
"jest-alias-preprocessor": "^1.1.1",
"jest-cli": "^20.0.4",
"jest-diff": "^20.0.3",
"jest-junit": "^3.0.0",
"jest-matcher-utils": "^20.0.3",
"jest-snapshot": "^20.0.3",
"jest-styled-components": "^4.4.1",
"jest-transform-graphql": "^2.1.0",
"lodash.sortby": "^4.7.0",
"mz": "^2.6.0",
"react-scripts": "^1.0.12",
"react-test-renderer": "^15.6.1",
"redrun": "^5.9.17",
"stylelint": "^8.0.0",
"stylelint-config-primer": "^2.0.1",
"stylelint-config-standard": "^17.0.0",
"stylelint-processor-styled-components": "styled-components/stylelint-processor-styled-components#2a33b5f"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,30 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>Joyent CoPilot</title>
<style>
.CodeMirror, .ReactCodeMirror {
height: 100% !important;
}
.CodeMirror {
border: solid 1px #d8d8d8;
}
html, body, #root {
height: 100%;
}
#root {
display: flex;
flex-flow: column;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1,120 +0,0 @@
const webpack = require('webpack');
const isString = require('lodash.isstring');
const fs = require('fs');
const path = require('path');
const FRONTEND_ROOT = process.cwd();
const FRONTEND = path.join(FRONTEND_ROOT, 'src');
const BabelLoader = loader => ({
test: loader.test,
include: loader.include,
loader: loader.loader,
options: {
babelrc: true,
cacheDirectory: true
}
});
const FileLoader = loader => ({
exclude: loader.exclude.concat([/\.(graphql|gql)$/]),
loader: loader.loader,
options: loader.options
});
module.exports = config => {
config.resolve.plugins = [];
config.plugins = config.plugins.filter(
plugin => !(plugin instanceof webpack.optimize.UglifyJsPlugin)
);
config.module.rules = config.module.rules
.reduce((loaders, loader, index) => {
if (Array.isArray(loader.use)) {
return loaders.concat([
Object.assign(loader, {
use: loader.use.map(l => {
if (isString(l) || !isString(l.loader)) {
return l;
}
if (!l.loader.match(/eslint-loader/)) {
return l;
}
return Object.assign(l, {
options: Object.assign(l.options, {
baseConfig: null,
useEslintrc: true
})
});
})
})
]);
}
if (Array.isArray(loader.oneOf)) {
return loaders.concat([
Object.assign(loader, {
oneOf: loader.oneOf.map(loader => {
if (!isString(loader.loader)) {
return loader;
}
if (loader.loader.match(/babel-loader/)) {
return BabelLoader(loader);
}
if (loader.loader.match(/file-loader/)) {
return FileLoader(loader);
}
return loader;
})
})
]);
}
if (!isString(loader.loader)) {
return loaders.concat([loader]);
}
if (loader.loader.match(/babel-loader/)) {
return loaders.concat(BabelLoader(loader));
}
if (loader.loader.match(/file-loader/)) {
return loaders.concat([FileLoader(loader)]);
}
return loaders.concat([loader]);
}, [])
.concat([
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: require.resolve('graphql-tag/loader')
}
]);
config.resolve.alias = Object.assign(
{},
config.resolve.alias,
fs
.readdirSync(FRONTEND)
.map(name => path.join(FRONTEND, name))
.filter(fullpath => fs.statSync(fullpath).isDirectory())
.reduce(
(aliases, fullpath) =>
Object.assign(aliases, {
[`@${path.basename(fullpath)}`]: fullpath
}),
{
'@root': FRONTEND
}
)
);
return config;
};

View File

@ -1,41 +0,0 @@
const { readFile, writeFile, exists } = require('mz/fs');
const main = require('apr-main');
const forEach = require('apr-for-each');
const path = require('path');
const ROOT = path.join(__dirname, '../../../node_modules/react-scripts/config');
const configs = ['webpack.config.dev', 'webpack.config.prod'];
const toCopy = [
'patch-webpack-config',
'webpack.config.dev',
'webpack.config.prod'
];
const backup = async file => {
const backupPath = path.join(ROOT, `${file}.original.js`);
const backupExists = await exists(backupPath);
if (backupExists) {
return;
}
const originalPath = path.join(ROOT, `${file}.js`);
const orignalConfig = await readFile(originalPath, 'utf-8');
return writeFile(backupPath, orignalConfig);
};
const copy = async file => {
const srcPath = path.join(__dirname, `${file}.js`);
const destPath = path.join(ROOT, `${file}.js`);
const src = await readFile(srcPath, 'utf-8');
return writeFile(destPath, src);
};
main(
(async () => {
await forEach(configs, backup);
await forEach(toCopy, copy);
})()
);

View File

@ -1,4 +0,0 @@
const originalConfig = require('./webpack.config.dev.original');
const patch = require('./patch-webpack-config');
module.exports = patch(originalConfig);

View File

@ -1,4 +0,0 @@
const originalConfig = require('./webpack.config.prod.original');
const patch = require('./patch-webpack-config');
module.exports = patch(originalConfig);

View File

@ -1,26 +0,0 @@
import React, { Component } from 'react';
import { ThemeProvider, injectGlobal } from 'styled-components';
import { theme, global } from 'joyent-ui-toolkit';
import { ApolloProvider } from 'react-apollo';
import { client, store } from '@state/store';
import Router from '@root/router';
class App extends Component {
componentWillMount() {
// eslint-disable-next-line no-unused-expressions
injectGlobal`
${global}
`;
}
render() {
return (
<ApolloProvider client={client} store={store}>
<ThemeProvider theme={theme}>{Router}</ThemeProvider>
</ApolloProvider>
);
}
}
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,32 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalHeading, ModalText, Button } from 'joyent-ui-toolkit';
const DeploymentGroupDelete = ({
deploymentGroup,
onCancelClick = () => {},
onConfirmClick = () => {}
}) => (
<div>
<ModalHeading>
Deleting a deployment group: <br /> {deploymentGroup.name}
</ModalHeading>
<ModalText marginBottom="3">
Deleting a deployment group will also remove all of the services and
instances associated with that deployment group. Are you sure you want to
continue?
</ModalText>
<Button onClick={onCancelClick} secondary>
Cancel
</Button>
<Button onClick={onConfirmClick}>Delete deployment group</Button>
</div>
);
DeploymentGroupDelete.propTypes = {
deploymentGroup: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
onConfirmClick: PropTypes.func
};
export default DeploymentGroupDelete;

View File

@ -1 +0,0 @@
export { default as DeploymentGroupDelete } from './delete';

View File

@ -1,12 +0,0 @@
import React from 'react';
import { Col, Row } from 'react-styled-flexboxgrid';
import { P } from 'joyent-ui-toolkit';
export default () => (
<Row>
<Col xs={12}>
<P>You don't have any instances</P>
</Col>
</Row>
);

View File

@ -1,2 +0,0 @@
export { default as EmptyInstances } from './empty';
export { default as InstanceListItem } from './list-item';

View File

@ -1,161 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import remcalc from 'remcalc';
import styled from 'styled-components';
import { isOr } from 'styled-is';
import titleCase from 'title-case';
import {
Card,
CardInfo,
CardView,
CardTitle,
CardDescription,
HealthyIcon,
Label
} from 'joyent-ui-toolkit';
const STATUSES = [
'PROVISIONING',
'READY',
'ACTIVE',
'RUNNING',
'STOPPING',
'STOPPED',
'OFFLINE',
'DELETED',
'DESTROYED',
'FAILED',
'INCOMPLETE',
'UNKNOWN'
];
const Dot = styled.span`
margin-right: ${remcalc(6)};
width: ${remcalc(6)};
height: ${remcalc(6)};
border-radius: ${remcalc(3)};
display: inline-block;
${isOr('provisioning', 'ready', 'active', 'running')`
background-color: ${props => props.theme.green};
`};
${isOr('stopping', 'stopped')`
background-color: ${props => props.theme.grey};
`};
${isOr('offline', 'destroyed', 'failed')`
background-color: ${props => props.theme.red};
`};
${isOr('deleted', 'incomplete', 'unknown')`
background-color: ${props => props.theme.secondaryActive};
`};
`;
const StyledCard = Card.extend`
flex-direction: row;
&:not(:last-child) {
margin-bottom: 0;
box-shadow: none;
border-bottom-width: 0;
}
background-color: ${props => props.theme.white};
${isOr(
'stopping',
'stopped',
'offline',
'destroyed',
'failed',
'deleted',
'incomplete',
'unknown'
)`
background-color: ${props => props.theme.background};
& [name="card-options"] > button {
background-color: ${props => props.theme.background};
}`
}
`;
const StatusContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-content: center;
`;
const InstanceCard = ({
instance,
onHealthMouseOver = () => {},
onStatusMouseOver = () => {},
onMouseOut = () => {}
}) => {
const statusProps = STATUSES.reduce(
(acc, name) =>
Object.assign(acc, {
[name.toLowerCase()]: name === instance.status
}),
{}
);
const label = (instance.healthy || 'UNKNOWN').toLowerCase();
const icon = <HealthyIcon healthy={instance.healthy} />;
const handleHealthMouseOver = evt => {
onHealthMouseOver(evt, instance);
};
const handleStatusMouseOver = evt => {
onStatusMouseOver(evt, instance);
};
const handleMouseOut = evt => {
onMouseOut(evt);
};
return (
<StyledCard collapsed={true} key={instance.uuid} {...statusProps}>
<CardView>
<CardTitle>{instance.name}</CardTitle>
<CardDescription>
<CardInfo
icon={icon}
iconPosition="left"
label={label}
color="dark"
onMouseOver={handleHealthMouseOver}
onMouseOut={handleMouseOut}
/>
</CardDescription>
<CardDescription>
<StatusContainer
onMouseOver={handleStatusMouseOver}
onMouseOut={handleMouseOut}
>
<Label>
<Dot {...statusProps} />
{titleCase(instance.status)}
</Label>
</StatusContainer>
</CardDescription>
</CardView>
</StyledCard>
);
};
InstanceCard.propTypes = {
instance: PropTypes.object.isRequired,
onHealthMouseOver: PropTypes.func,
onStatusMouseOver: PropTypes.func,
onMouseOut: PropTypes.func
};
export default InstanceCard;

View File

@ -1,22 +0,0 @@
import { Grid } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import is, { isNot } from 'styled-is';
export default Grid.extend`
padding-top: ${remcalc(19)};
${isNot('plain')`
flex: 1 1 auto;
display: block;
flex-flow: column;
`};
${is('center')`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-content: center;
align-items: center;
`};
`;

View File

@ -1 +0,0 @@
export { default as LayoutContainer } from './container';

View File

@ -1,20 +0,0 @@
import React from 'react';
import ManifestEditorBundle from './manifest-editor';
export const MEditor = ({ input, defaultValue, readOnly }) => (
<ManifestEditorBundle
mode="yaml"
{...input}
value={input.value || defaultValue}
readOnly={readOnly}
/>
);
export const EEditor = ({ input, defaultValue, readOnly }) => (
<ManifestEditorBundle
mode="ini"
{...input}
value={input.value || defaultValue}
readOnly={readOnly}
/>
);

View File

@ -1,83 +0,0 @@
import React from 'react';
import { Field } from 'redux-form';
import remcalc from 'remcalc';
import { Row } from 'react-styled-flexboxgrid';
import { Button, Divider, H3, P } from 'joyent-ui-toolkit';
import { EEditor } from './editors';
import Files from './files';
const EnvironmentDivider = Divider.extend`margin-top: ${remcalc(34)};`;
const ButtonsRow = Row.extend`margin: ${remcalc(29)} 0 ${remcalc(60)} 0;`;
const Subtitle = H3.extend`
margin-top: ${remcalc(34)};
margin-bottom: ${remcalc(3)};
`;
const Description = P.extend`
margin-top: ${remcalc(3)};
margin-bottom: ${remcalc(20)};
`;
export const Environment = ({
handleSubmit,
onCancel,
onAddFile,
onRemoveFile,
dirty,
defaultValue = '',
files = [],
readOnly = false,
loading
}) => {
const envEditor = !readOnly ? (
<Field name="environment" defaultValue={defaultValue} component={EEditor} />
) : (
<EEditor input={{ value: defaultValue }} readOnly />
);
const footerDivider = !readOnly ? <EnvironmentDivider /> : null;
const footer = !readOnly ? (
<ButtonsRow>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
loading={loading}
type="submit"
>
Continue
</Button>
</ButtonsRow>
) : null;
return (
<form onSubmit={handleSubmit}>
<Subtitle>Global variables</Subtitle>
<Description>
These variables are going to be availabe for interpolation in the
manifest
</Description>
{envEditor}
<EnvironmentDivider />
<Subtitle>Enviroment files</Subtitle>
<Description>
The variables from this files will be applied to the services that
require them
</Description>
<Files
files={files}
onAddFile={onAddFile}
onRemoveFile={onRemoveFile}
readOnly={readOnly}
/>
{footerDivider}
{footer}
</form>
);
};
export default Environment;

View File

@ -1,94 +0,0 @@
import React from 'react';
import { Field } from 'redux-form';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { EEditor } from './editors';
import { FormGroup, Input, Button, Card } from 'joyent-ui-toolkit';
const FilenameContainer = styled.span`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: stretch;
`;
const FilenameInput = styled(Input)`
order: 0;
flex: 1 1 auto;
align-self: stretch;
margin: 0 0 ${remcalc(13)} 0;
`;
const FilenameRemove = Button.extend`
order: 0;
flex: 0 1 auto;
align-self: auto;
margin: 0 0 0 ${remcalc(8)};
height: ${remcalc(48)};
`;
const FileCard = Card.extend`padding: ${remcalc(24)} ${remcalc(19)};`;
const File = ({ id, name, value, onRemoveFile, readOnly }) => {
const removeButton = !readOnly ? (
<FilenameRemove type="button" onClick={onRemoveFile} secondary>
Remove
</FilenameRemove>
) : null;
const fileEditor = !readOnly ? (
<Field name={`file-value-${id}`} defaultValue={value} component={EEditor} />
) : (
<EEditor input={{ value }} readOnly />
);
const input = !readOnly ? (
<FilenameInput type="text" placeholder="Filename including extension…" />
) : (
<FilenameInput
type="text"
placeholder="Filename including extension…"
value={name}
/>
);
return (
<FileCard>
<FormGroup name={`file-name-${id}`} reduxForm={!readOnly}>
<FilenameContainer>
{input}
{removeButton}
</FilenameContainer>
</FormGroup>
{fileEditor}
</FileCard>
);
};
const Files = ({ files, onAddFile, onRemoveFile, readOnly }) => {
const footer = !readOnly ? (
<Button type="button" onClick={onAddFile} secondary>
Create new .env file
</Button>
) : null;
return (
<div>
{files.map(({ id, ...rest }) => (
<File
key={id}
id={id}
onRemoveFile={() => onRemoveFile(id)}
readOnly={readOnly}
{...rest}
/>
))}
{footer}
</div>
);
};
export default Files;

View File

@ -1,8 +0,0 @@
export { default as Files } from './files';
export { default as Editors } from './editors';
export { default as ManifestEditorBundle } from './manifest-editor';
export { default as Manifest } from './manifest';
export { default as Name } from './name';
export { default as Review } from './review';
export { default as Progress } from './progress';
export { default as Environment } from './environment';

View File

@ -1,39 +0,0 @@
import React, { Component } from 'react';
import Bundle from 'react-bundle';
import { Loader } from '@components/messaging';
class ManifestEditorBundle extends Component {
constructor() {
super();
this.state = {};
this.handleRender = this.handleRender.bind(this);
}
handleRender(ManifestEditor) {
if (ManifestEditor) {
setTimeout(() => {
this.setState({ ManifestEditor });
}, 80);
}
return <Loader />;
}
render() {
if (!this.state.ManifestEditor) {
return (
<Bundle load={() => import('joyent-manifest-editor')}>
{this.handleRender}
</Bundle>
);
}
const { ManifestEditor } = this.state;
const { children, ...rest } = this.props;
return <ManifestEditor {...rest}>{children}</ManifestEditor>;
}
}
export default ManifestEditorBundle;

View File

@ -1,41 +0,0 @@
import React from 'react';
import { FormGroup, FormMeta, Button, FormLabel } from 'joyent-ui-toolkit';
import { Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import { Field } from 'redux-form';
import { MEditor } from './editors';
const ButtonsRow = Row.extend`
margin: ${remcalc(29)} 0 ${remcalc(60)} 0;
`;
export const Manifest = ({
handleSubmit,
onCancel,
dirty,
defaultValue = '',
loading
}) => (
<form onSubmit={handleSubmit}>
<FormGroup reduxForm>
<FormMeta left>
<FormLabel>Project manifest</FormLabel>
</FormMeta>
<Field name="manifest" defaultValue={defaultValue} component={MEditor} />
</FormGroup>
<ButtonsRow>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
loading={loading}
type="submit"
>
Environment
</Button>
</ButtonsRow>
</form>
);
export default Manifest;

View File

@ -1,42 +0,0 @@
import React from 'react';
import { Row, Col } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import {
FormMeta,
Button,
FormLabel,
Input,
Small,
FormGroup
} from 'joyent-ui-toolkit';
const ButtonsRow = Row.extend`margin: ${remcalc(29)} 0 ${remcalc(60)} 0;`;
export const Name = ({ handleSubmit, onCancel, dirty }) => (
<form onSubmit={handleSubmit}>
<Row>
<Col xs={12} md={4} lg={4}>
<FormGroup name="name" reduxForm>
<FormMeta left>
<FormLabel>Name the new deployment group</FormLabel>
<Small>
Your services will be deployed to eu-east-1 data center.
</Small>
</FormMeta>
<Input type="text" />
</FormGroup>
</Col>
</Row>
<ButtonsRow>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button type="submit" disabled={!dirty}>
Next
</Button>
</ButtonsRow>
</form>
);
export default Name;

View File

@ -1,77 +0,0 @@
import React from 'react';
import {
Progressbar,
ProgressbarItem,
ProgressbarButton
} from 'joyent-ui-toolkit';
const Progress = ({ stage, create, edit }) => {
const _nameCompleted = stage !== 'name';
const _nameActive = stage === 'name';
const _name = !create ? null : (
<ProgressbarItem>
<ProgressbarButton
zIndex="10"
completed={_nameCompleted}
active={_nameActive}
first
>
Name the group
</ProgressbarButton>
</ProgressbarItem>
);
const _manifestCompleted = ['environment', 'review'].indexOf(stage) >= 0;
const _manifestActive = create ? stage === 'manifest' : stage === 'edit';
const _manifest = (
<ProgressbarItem>
<ProgressbarButton
zIndex="9"
completed={_manifestCompleted}
active={_manifestActive}
first={edit}
>
Define Services
</ProgressbarButton>
</ProgressbarItem>
);
const _environmentCompleted = stage === 'review';
const _environmentActive = stage === 'environment';
const _environment = (
<ProgressbarItem>
<ProgressbarButton
zIndex="8"
completed={_environmentCompleted}
active={_environmentActive}
>
Define Environment
</ProgressbarButton>
</ProgressbarItem>
);
const _reviewActive = stage === 'review';
const _review = (
<ProgressbarItem>
<ProgressbarButton zIndex="7" active={_reviewActive} last>
Review and deploy
</ProgressbarButton>
</ProgressbarItem>
);
return (
<Progressbar>
{_name}
{_manifest}
{_environment}
{_review}
</Progressbar>
);
};
export default Progress;

View File

@ -1,123 +0,0 @@
import React from 'react';
import remcalc from 'remcalc';
import { Row } from 'react-styled-flexboxgrid';
import is from 'styled-is';
import {
Button,
Divider,
H3,
P,
Chevron,
typography,
Card
} from 'joyent-ui-toolkit';
import forceArray from 'force-array';
import styled from 'styled-components';
import { EEditor } from './editors';
const ButtonsRow = Row.extend`margin: ${remcalc(29)} 0 ${remcalc(60)} 0;`;
const ServiceEnvironmentTitle = P.extend`
margin: ${remcalc(13)} 0 0 0;
${is('expanded')`
margin-bottom: ${remcalc(13)};
`};
`;
const ServiceName = H3.extend`
margin-top: 0;
margin-bottom: ${remcalc(5)};
line-height: 1.6;
font-size: ${remcalc(18)};
`;
const ImageTitle = H3.extend`
display: inline-block;
margin: 0;
`;
const Image = styled.span`
${typography.fontFamily};
font-size: ${remcalc(15)};
`;
const ServiceDivider = Divider.extend`
margin: ${remcalc(13)} ${remcalc(-20)} 0 ${remcalc(-20)};
`;
const ServiceCard = Card.extend`
padding: ${remcalc(13)} ${remcalc(19)};
min-height: initial;
`;
const Dl = styled.dl`margin: 0;`;
const EnvironmentChevron = styled(Chevron)`float: right;`;
const EnvironmentReview = ({ environment }) => {
const value = environment
.map(({ name, value }) => `${name}=${value}`)
.join('\n');
return <EEditor input={{ value }} />;
};
export const Review = ({
handleSubmit,
onEnvironmentToggle = () => null,
onCancel,
dirty,
loading,
environmentToggles,
...state
}) => {
const serviceList = forceArray(state.services).map(({ name, config }) => (
<ServiceCard key={name}>
<ServiceName>{name}</ServiceName>
<Dl>
<dt>
<ImageTitle>Image:</ImageTitle> <Image>{config.image}</Image>
</dt>
</Dl>
{config.environment && config.environment.length ? (
<ServiceDivider />
) : null}
{config.environment && config.environment.length ? (
<ServiceEnvironmentTitle
expanded={environmentToggles[name]}
onClick={() => onEnvironmentToggle(name)}
>
Environment variables{' '}
<EnvironmentChevron
down={!environmentToggles[name]}
up={environmentToggles[name]}
/>
</ServiceEnvironmentTitle>
) : null}
{config.environment &&
config.environment.length &&
environmentToggles[name] ? (
<EnvironmentReview environment={config.environment} />
) : null}
</ServiceCard>
));
return (
<form onSubmit={handleSubmit}>
{serviceList}
<ButtonsRow>
<Button type="button" onClick={onCancel} disabled={loading} secondary>
Cancel
</Button>
<Button disabled={loading} loading={loading} type="submit">
Confirm and Deploy
</Button>
</ButtonsRow>
</form>
);
};
export default Review

View File

@ -1,14 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Message } from 'joyent-ui-toolkit';
const ErrorMessage = ({ title, message = "Ooops, there's been an error" }) => (
<Message title={title} message={message} type="ERROR" />
);
ErrorMessage.propTypes = {
title: PropTypes.string,
message: PropTypes.string
};
export default ErrorMessage;

View File

@ -1,4 +0,0 @@
export { default as Loader } from './loader';
export { default as ErrorMessage } from './error';
export { default as WarningMessage } from './warning';
export { default as ModalErrorMessage } from './modal-error';

View File

@ -1,35 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { P, StatusLoader } from 'joyent-ui-toolkit';
const Container = styled.div`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-content: center;
align-items: center;
flex: 1 0 auto;
align-self: stretch;
`;
const Loader = styled(StatusLoader)`
flex: 0 0 auto;
align-self: stretch;
`;
const Msg = P.extend`
flex: 0 0 auto;
align-self: stretch;
text-align: center;
margin-bottom: 0;
`;
export default ({ msg }) => (
<Container>
<Loader />
<Msg>{msg || 'Loading...'}</Msg>
</Container>
);

View File

@ -1,26 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { ModalHeading, ModalText, Button } from 'joyent-ui-toolkit';
const StyledHeading = styled(ModalHeading)`
color: ${props => props.theme.red};
`;
const ModalErrorMessage = ({ title, message, onCloseClick }) => (
<div>
<StyledHeading>{title}</StyledHeading>
<ModalText marginBottom="3">{message}</ModalText>
<Button onClick={onCloseClick} secondary>
Close{' '}
</Button>
</div>
);
ModalErrorMessage.propTypes = {
title: PropTypes.string,
message: PropTypes.string.isRequired,
onCloseClick: PropTypes.func.isRequired
};
export default ModalErrorMessage;

View File

@ -1,14 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Message } from 'joyent-ui-toolkit';
const WarningMessage = ({ title, message }) => (
<Message title={title} message={message} type="WARNING" />
);
WarningMessage.propTypes = {
title: PropTypes.string,
message: PropTypes.string.isRequired
};
export default WarningMessage;

View File

@ -1,55 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { Grid, Col } from 'react-styled-flexboxgrid';
import { Link } from 'react-router-dom';
import forceArray from 'force-array';
import PropTypes from 'prop-types';
import remcalc from 'remcalc';
import { Breadcrumb, BreadcrumbItem } from 'joyent-ui-toolkit';
const BreadcrumbLink = styled(Link)`
text-decoration: none;
cursor: pointer;
&:visited {
color: inherit;
}
`;
const BreadcrumbContainer = styled.div`
border-bottom: solid ${remcalc(1)} ${props => props.theme.grey};
`;
const getBreadcrumbItems = (...links) =>
forceArray(links).map(({ pathname, name }, i) => {
const item =
i + 1 >= links.length ? (
name
) : (
<BreadcrumbLink to={pathname}>{name}</BreadcrumbLink>
);
return <BreadcrumbItem key={name}>{item}</BreadcrumbItem>;
});
const NavBreadcrumb = ({ links = [] }) => (
<BreadcrumbContainer>
<Grid>
<Col xs={12}>
<Breadcrumb>{getBreadcrumbItems(...links)}</Breadcrumb>
</Col>
</Grid>
</BreadcrumbContainer>
);
NavBreadcrumb.propTypes = {
links: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
pathname: PropTypes.string
})
)
};
export default NavBreadcrumb;

View File

@ -1,48 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Img } from 'normalized-styled-components';
import remcalc from 'remcalc';
import styled from 'styled-components';
import Logo from '@assets/triton_logo.png';
import {
Header,
HeaderBrand,
HeaderItem,
DataCenterIcon,
UserIcon
} from 'joyent-ui-toolkit';
const StyledLogo = Img.extend`
width: ${remcalc(87)};
height: ${remcalc(25)};
`;
const Item = styled.span`
padding-left: 5px;
`;
const NavHeader = ({ datacenter, username }) => (
<Header>
<HeaderBrand>
<Link to="/">
<StyledLogo src={Logo} />
</Link>
</HeaderBrand>
<HeaderItem>
<DataCenterIcon />
<Item>{datacenter}</Item>
</HeaderItem>
<HeaderItem>
<UserIcon />
<Item>{username}</Item>
</HeaderItem>
</Header>
);
NavHeader.propTypes = {
datacenter: PropTypes.string,
username: PropTypes.string
};
export default NavHeader;

View File

@ -1,5 +0,0 @@
export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';
export { default as Header } from './header';
export { default as Title } from './title';
export { default as NotFound } from './not-found';

View File

@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import forceArray from 'force-array';
import { LayoutContainer } from '@components/layout';
import {
SectionList,
SectionListItem,
SectionListNavLink
} from 'joyent-ui-toolkit';
const getMenuItems = (...links) =>
forceArray(links).map(({ pathname, name }) => (
<SectionListItem key={pathname}>
<SectionListNavLink activeClassName="active" to={pathname}>
{name}
</SectionListNavLink>
</SectionListItem>
));
const Menu = ({ links = [] }) => (
<LayoutContainer plain>
<SectionList>{getMenuItems(...links)}</SectionList>
</LayoutContainer>
);
Menu.propTypes = {
links: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
pathname: PropTypes.string
})
)
};
export default Menu;

View File

@ -1,44 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { H1, P, Button } from 'joyent-ui-toolkit';
import { LayoutContainer } from '@components/layout';
const StyledContainer = styled.div`
margin-top: ${remcalc(60)};
`;
const StyledTitle = styled(H1)`
font-weight: normal;
font-size: ${remcalc(32)};
`;
const StyledP = styled(P)`
margin-bottom: ${remcalc(30)};
max-width: ${remcalc(490)};
`;
const NotFound = ({
title = 'I have no memory of this place',
message = 'HTTP 404: We cant find what you are looking for. Next time, always follow your nose.',
link = 'Back to dashboard',
to = '/deployment-groups'
}) => (
<LayoutContainer>
<StyledContainer>
<StyledTitle>{title}</StyledTitle>
<StyledP>{message}</StyledP>
<Button to={to}>{link}</Button>
</StyledContainer>
</LayoutContainer>
);
NotFound.propTypes = {
title: PropTypes.string,
message: PropTypes.string,
link: PropTypes.string,
to: PropTypes.string
};
export default NotFound;

View File

@ -1,8 +0,0 @@
import { H2 } from 'joyent-ui-toolkit';
import remcalc from 'remcalc';
export default H2.extend`
margin-top: ${remcalc(2)};
flex: 0 0 auto;
align-self: stretch;
`;

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalHeading, ModalText, Button } from 'joyent-ui-toolkit';
const ServiceDelete = ({
service,
onCancelClick = () => {},
onConfirmClick = () => {}
}) => (
<div>
<ModalHeading>
Deleting a service: <br /> {service.name}
</ModalHeading>
<ModalText marginBottom="3">
Deleting a service can lead to irreversible loss of data and failures in
your application. Are you sure you want to continue?
</ModalText>
<Button onClick={onCancelClick} secondary>
Cancel
</Button>
<Button onClick={onConfirmClick}>Delete service</Button>
</div>
);
ServiceDelete.propTypes = {
service: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
onConfirmClick: PropTypes.func
};
export default ServiceDelete;

View File

@ -1,3 +0,0 @@
export { default as ServiceScale } from './scale';
export { default as ServiceDelete } from './delete';
export { default as ServiceMetrics } from './metrics';

View File

@ -1,52 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import remcalc from 'remcalc';
import {
MetricGraph,
Card,
CardView,
CardTitle,
CardHeader
} from 'joyent-ui-toolkit';
const MetricView = styled(CardView)`
padding-top: ${remcalc(48)};
& canvas {
margin: 0 auto;
}
`;
const ServiceMetrics = ({ metricsData, graphDurationSeconds }) => {
// metricsData should prob be an array rather than an object
// should also have a header, w metric name and number of instances (omit everything else from design for copilot)
const metricGraphs = Object.keys(metricsData).map(key => (
<Card key={key} headed active>
<CardHeader>
<CardTitle>{key}</CardTitle>
</CardHeader>
<MetricView>
<MetricGraph
key={key}
metricsData={metricsData[key]}
graphDurationSeconds={graphDurationSeconds}
displayY
displayX
/>
</MetricView>
</Card>
));
// This needs layout!!!
return <div>{metricGraphs}</div>;
};
ServiceMetrics.propTypes = {
// metricsData should prob be an array rather than an object
metricsData: PropTypes.object.isRequired,
graphDurationSeconds: PropTypes.number.isRequired
};
export default ServiceMetrics;

View File

@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalHeading, ModalText, Button } from 'joyent-ui-toolkit';
import {
FormGroup,
NumberInput,
NumberInputNormalize,
FormMeta
} from 'joyent-ui-toolkit';
const ServiceScale = ({
service,
handleSubmit = () => {},
onCancel = () => {},
invalid,
pristine
}) => (
<form onSubmit={handleSubmit}>
<ModalHeading>
Scaling a service: <br />
{service.name}
</ModalHeading>
<ModalText>
Choose how many instances of a service you want to have running.
</ModalText>
<FormGroup
name="replicas"
normalize={NumberInputNormalize({ minValue: 1 })}
reduxForm
>
<FormMeta />
<NumberInput minValue={1} />
</FormGroup>
<Button type="button" secondary onClick={onCancel}>
Cancel
</Button>
<Button type="submit" disabled={pristine || invalid} secondary>
Scale
</Button>
</form>
);
ServiceScale.propTypes = {
service: PropTypes.object.isRequired,
onSubmitClick: PropTypes.func,
onCancelClick: PropTypes.func
};
export default ServiceScale;

View File

@ -1,60 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { LayoutContainer } from '@components/layout';
import { Col, Row } from 'react-styled-flexboxgrid';
import { Button, P, H2, H3 } from 'joyent-ui-toolkit';
const StyledBox = styled.div`
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.05);
border: solid 1px #d8d8d8;
padding: ${remcalc('6 18 24')};
& + & {
margin-top: ${remcalc(24)};
}
`;
export default () => (
<LayoutContainer>
<Row>
<Col>
<H2>Services</H2>
<Row>
<Col>
<StyledBox>
<Row>
<Col md={10}>
<H3>Import your services</H3>
<P>
You can import your services from a Git repository hosting
service. Learn more.
</P>
<Button secondary>from GitHub</Button>
<Button secondary>from GitLab</Button>
<Button secondary>from BitBucket</Button>
</Col>
</Row>
</StyledBox>
<StyledBox>
<Row>
<Col md={9}>
<H3>Alternatively, you can upload or edit manifest file.</H3>
<P>
Manifest is a file describing your services. It is similar
to Docker Compose file. You can upload a file from you local
machine or edit it manually. Learn more.
</P>
<Button secondary>Upload manifest</Button>
<Button secondary>Edit manifest</Button>
</Col>
</Row>
</StyledBox>
</Col>
</Row>
</Col>
</Row>
</LayoutContainer>
);

View File

@ -1,3 +0,0 @@
export { default as EmptyServices } from './empty';
export { default as ServiceListItem } from './list-item';
export { default as ServicesQuickActions } from './quick-actions';

View File

@ -1,232 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
import { isNot } from 'styled-is';
import { Col, Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import { InstancesIcon, HealthyIcon } from 'joyent-ui-toolkit';
import Status from './status';
import {
Small,
MetricGraph,
Card,
CardView,
CardTitle,
CardDescription,
CardGroupView,
CardOptions,
CardHeader,
CardInfo,
Anchor
} from 'joyent-ui-toolkit';
const StyledCardHeader = styled(CardHeader)`
position: relative;
`;
const TitleInnerContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
`;
const StyledAnchor = styled(Anchor)`
${isNot('active')`
color: ${props => props.theme.text}
`};
`;
const GraphsContainer = styled(Row)`
background: #f6f7fe;
width: 50%;
margin: 0;
flex: 1;
`;
const GraphContainer = styled(Col)`
position: relative;
border-left: ${remcalc(1)} solid #d8d8d8;
padding-top: ${remcalc(20)};
`;
const GraphLeftShaddow = styled.div`
z-index: 99;
position: absolute;
margin-left: ${remcalc(-8)};
margin-top: ${remcalc(-20)};
width: ${remcalc(12)};
height: 100%;
background-image: linear-gradient(
to right,
rgba(213, 216, 231, 0.8),
rgba(243, 244, 249, 0)
);
`;
const GraphTitle = Small.extend`
z-index: 99;
position: absolute;
top: 0;
left: 0;
right: 0;
height: ${remcalc(20)};
border-bottom: ${remcalc(1)} solid #d8d8d8;
font-size: ${remcalc(13)};
text-align: center;
color: #494949;
`;
const ChildTitle = styled(CardTitle)`
padding: 0;
flex: 0 1 auto;
align-self: stretch;
`;
const ServiceView = styled(CardView)`
height: ${remcalc(120)};
`;
const StatusContainer = styled(CardDescription)`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-content: center;
align-items: stretch;
`;
const HealthInfoContainer = styled.div`
flex: 0 1 auto;
align-self: flex-end;
position: absolute;
bottom: 0;
`;
const ServiceListItem = ({
onQuickActionsClick = () => {},
deploymentGroup = '',
service,
isChild = false
}) => {
const handleCardOptionsClick = evt => {
onQuickActionsClick(evt, service);
};
const children = sortBy(forceArray(service.children), ['slug']);
// const isServiceInactive = service.status && service.status !== 'ACTIVE';
const to = `/deployment-groups/${deploymentGroup}/services/${service.slug}`;
const instancesCount = children.length
? children.reduce((count, child) => count + child.instances.length, 0)
: service.instances.length;
const childrenItems = children.length
? children.map(service => (
<ServiceListItem
key={service.id}
deploymentGroup={deploymentGroup}
service={service}
isChild
/>
))
: null;
const title = isChild ? (
<ChildTitle>{service.name}</ChildTitle>
) : (
<CardTitle>
<TitleInnerContainer>
<StyledAnchor to={to} secondary active={service.instancesActive}>
{service.name}
</StyledAnchor>
</TitleInnerContainer>
</CardTitle>
);
const header = !isChild ? (
<StyledCardHeader>
{title}
<CardDescription>
<CardInfo
icon={<InstancesIcon />}
iconPosition="left"
label={`${instancesCount} ${instancesCount > 1
? 'instances'
: 'instance'}`}
color={!service.instancesActive ? 'disabled' : 'light'}
/>
</CardDescription>
<CardOptions onClick={handleCardOptionsClick} />
</StyledCardHeader>
) : null;
let healthyInfo = null;
if (service.instancesActive) {
const { total, healthy } = service.instancesHealthy;
const iconHealthy = total === healthy ? 'HEALTHY' : 'NOT HEALTHY';
const icon = <HealthyIcon healthy={iconHealthy} />;
const label = `${healthy} of ${total} healthy`;
healthyInfo = (
<CardInfo icon={icon} iconPosition="left" label={label} color="dark" />
);
}
const graphs =
!children.length && service.metrics && Object.keys(service.metrics).length
? Object.keys(service.metrics).map(key => (
<GraphContainer xs={4}>
<GraphLeftShaddow />
<GraphTitle>{key}</GraphTitle>
<MetricGraph
key={key}
metricsData={service.metrics[key]}
graphDurationSeconds={90}
/>
</GraphContainer>
))
: null;
const metrics = graphs ? <GraphsContainer>{graphs}</GraphsContainer> : null;
const view = children.length ? (
<CardGroupView>{childrenItems}</CardGroupView>
) : (
<ServiceView>
<StatusContainer>
{isChild && title}
<Status service={service} />
<HealthInfoContainer>{healthyInfo}</HealthInfoContainer>
</StatusContainer>
{metrics}
</ServiceView>
);
return (
<Card
collapsed={service.collapsed}
active={service.instancesActive}
flat={isChild}
headed={!isChild}
key={service.id}
stacked={isChild && service.instances > 1}
>
{header}
{view}
</Card>
);
};
ServiceListItem.propTypes = {
onQuickActionsClick: PropTypes.func,
deploymentGroup: PropTypes.string,
service: PropTypes.object.isRequired // Define better
};
export default ServiceListItem;

View File

@ -1,109 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Tooltip,
TooltipButton,
TooltipDivider,
TooltipList
} from 'joyent-ui-toolkit';
const ServicesQuickActions = ({
show,
position,
service,
onBlur = () => {},
onRestartClick = () => {},
onStopClick = () => {},
onStartClick = () => {},
onScaleClick = () => {},
onDeleteClick = () => {}
}) => {
if (!show) {
return null;
}
const handleRestartClick = evt => {
onRestartClick(evt, service);
};
const handleStartClick = evt => {
onStartClick(evt, service);
};
const handleStopClick = evt => {
onStopClick(evt, service);
};
const handleScaleClick = evt => {
onScaleClick(evt, service);
};
const handleDeleteClick = evt => {
onDeleteClick(evt, service);
};
const disabled = service.transitionalStatus;
const status = service.instances.reduce((status, instance) => {
return status
? instance.status === status ? status : 'MIXED'
: instance.status;
}, null);
const startService =
status === 'RUNNING' ? null : (
<li>
<TooltipButton onClick={handleStartClick} disabled={disabled}>
Start
</TooltipButton>
</li>
);
const stopService =
status === 'STOPPED' ? null : (
<li>
<TooltipButton onClick={handleStopClick} disabled={disabled}>
Stop
</TooltipButton>
</li>
);
return (
<Tooltip {...position} onBlur={onBlur}>
<TooltipList>
<li>
<TooltipButton onClick={handleScaleClick} disabled={disabled}>
Scale
</TooltipButton>
</li>
<li>
<TooltipButton onClick={handleRestartClick} disabled={disabled}>
Restart
</TooltipButton>
</li>
{startService}
{stopService}
<TooltipDivider />
<li>
<TooltipButton onClick={handleDeleteClick} disabled={disabled}>
Delete
</TooltipButton>
</li>
</TooltipList>
</Tooltip>
);
};
ServicesQuickActions.propTypes = {
service: PropTypes.object.isRequired,
position: PropTypes.object,
show: PropTypes.bool,
onBlur: PropTypes.func,
onRestartClick: PropTypes.func,
onStopClick: PropTypes.func,
onStartClick: PropTypes.func,
onScaleClick: PropTypes.func,
onDeleteClick: PropTypes.func
};
export default ServicesQuickActions;

View File

@ -1,59 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { StatusLoader, P } from 'joyent-ui-toolkit';
const StyledStatusContainer = styled.div`
display: inline-block;
margin: 0;
flex: 1 1 auto;
align-self: stretch;
`;
const StyledStatus = P.extend`
margin: 0 0 ${remcalc(6)} 0;
font-size: ${remcalc(13)};
line-height: ${remcalc(13)};
`;
const StyledTransitionalStatus = StyledStatus.extend`
display: inline-block;
margin-left: ${remcalc(6)};
text-transform: capitalize;
`;
const ServiceStatus = ({ service }) => {
const getInstanceStatuses = instanceStatuses =>
instanceStatuses.map((instanceStatus, index) => {
const { status, count } = instanceStatus;
return (
<StyledStatus key={index}>
{`${count}
${count > 1 ? 'instances' : 'instance'}
${status.toLowerCase()}`}
</StyledStatus>
);
});
return service.transitionalStatus ? (
<StyledStatusContainer>
<StatusLoader />
<StyledTransitionalStatus>
{service.status ? service.status.toLowerCase() : ''}
</StyledTransitionalStatus>
</StyledStatusContainer>
) : (
<StyledStatusContainer>
{getInstanceStatuses(service.instanceStatuses)}
</StyledStatusContainer>
);
};
ServiceStatus.propTypes = {
service: PropTypes.object.isRequired
};
export default ServiceStatus;

View File

@ -1,123 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import DeploymentGroupDeleteMutation from '@graphql/DeploymentGroupDeleteMutation.gql';
import DeploymentGroupQuery from '@graphql/DeploymentGroup.gql';
import { Loader, ModalErrorMessage } from '@components/messaging';
import { DeploymentGroupDelete as DeploymentGroupDeleteComponent } from '@components/deployment-group';
import { Modal } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation';
export class DeploymentGroupDelete extends Component {
constructor(props) {
super(props);
this.state = {
error: null
};
}
render() {
const { history, match, loading, error } = this.props;
const handleCloseClick = evt => {
const closeUrl = match.url
.split('/')
.slice(0, -2)
.join('/');
history.replace(closeUrl);
};
if (loading) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<Loader />
</Modal>
);
}
if (error) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ModalErrorMessage
title="Ooops!"
message="An error occurred while loading your deployment group."
onCloseClick={handleCloseClick}
/>
</Modal>
);
}
const { deploymentGroup, deleteDeploymentGroup } = this.props;
if (this.state.error) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ModalErrorMessage
title="Ooops!"
message={`An error occurred while attempting to delete the ${deploymentGroup.name} deployment group.`}
onCloseClick={handleCloseClick}
/>
</Modal>
);
}
const handleConfirmClick = evt => {
deleteDeploymentGroup(deploymentGroup.id)
.then(() => handleCloseClick())
.catch(err => {
this.setState({ error: err });
});
};
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<DeploymentGroupDeleteComponent
deploymentGroup={deploymentGroup}
onConfirmClick={handleConfirmClick}
onCancelClick={handleCloseClick}
/>
</Modal>
);
}
}
DeploymentGroupDelete.propTypes = {
deploymentGroup: PropTypes.object,
history: PropTypes.object,
deleteDeploymentGroup: PropTypes.func.isRequired
};
const DeleteDeploymentGroupGql = graphql(DeploymentGroupDeleteMutation, {
props: ({ mutate }) => ({
deleteDeploymentGroup: deploymentGroupId =>
mutate({
variables: { id: deploymentGroupId }
})
})
});
const DeploymentGroupGql = graphql(DeploymentGroupQuery, {
options(props) {
const params = props.match.params;
const deploymentGroupSlug = params.deploymentGroup;
return {
variables: {
deploymentGroupSlug
}
};
},
props: ({ data: { deploymentGroup, loading, error } }) => ({
deploymentGroup,
loading,
error
})
});
const DeploymentGroupDeleteWithData = compose(
DeleteDeploymentGroupGql,
DeploymentGroupGql,
withNotFound([GqlPaths.DEPLOYMENT_GROUP])
)(DeploymentGroupDelete);
export default DeploymentGroupDeleteWithData;

View File

@ -1 +0,0 @@
export { default as DeploymentGroupDelete } from './delete';

View File

@ -1,25 +0,0 @@
import React from 'react';
import { graphql } from 'react-apollo';
import get from 'lodash.get';
import ManifestEditOrCreate from '@containers/manifest/edit-or-create';
import { Progress } from '@components/manifest';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import PortalQuery from '@graphql/Portal.gql';
const DeploymentGroupCreate = ({ match, dataCenter }) => (
<LayoutContainer>
<Title>Creating deployment group</Title>
<Progress stage={match.params.stage} create />
<ManifestEditOrCreate create dataCenter={dataCenter} />
</LayoutContainer>
);
const DeploymentGroupCreateWithData = graphql(PortalQuery, {
props: ({ data: { portal = {} } }) => ({
dataCenter: get(portal, 'datacenter.region', '')
})
})(DeploymentGroupCreate);
export default DeploymentGroupCreateWithData;

View File

@ -1,69 +0,0 @@
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import intercept from 'apr-intercept';
import DeploymentGroupImportMutation from '@graphql/DeploymentGroupImport.gql';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { ErrorMessage, Loader } from '@components/messaging';
class DeploymentGroupImport extends Component {
constructor() {
super();
this.state = {
error: false
};
setTimeout(this.importDeploymentGroup, 16);
}
importDeploymentGroup = async () => {
const { importDeploymentGroup, match, history } = this.props;
const { slug } = match.params;
const [error] = await intercept(
importDeploymentGroup({
slug
})
);
if (error) {
return this.setState({ loading: false, error });
}
history.push(`/deployment-groups/${slug}`);
};
render() {
const { error } = this.state;
const _title = <Title>Importing deployment group</Title>;
if (error) {
return (
<LayoutContainer>
{_title}
<ErrorMessage
title="Ooops!"
message="An error occurred while importing your deployment groups."
/>
</LayoutContainer>
);
}
return (
<LayoutContainer center>
{_title}
<Loader />
</LayoutContainer>
);
}
}
export default graphql(DeploymentGroupImportMutation, {
props: ({ mutate }) => ({
importDeploymentGroup: variables => mutate({ variables })
})
})(DeploymentGroupImport);

View File

@ -1,3 +0,0 @@
export { default as DeploymentGroupList } from './list';
export { default as DeploymentGroupCreate } from './create';
export { default as DeploymentGroupImport } from './import';

View File

@ -1,214 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Col, Row } from 'react-styled-flexboxgrid';
import forceArray from 'force-array';
import remcalc from 'remcalc';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { ErrorMessage, Loader } from '@components/messaging';
import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql';
import DeploymentGroupsImportableQuery from '@graphql/DeploymentGroupsImportable.gql';
import { H3, Small, IconButton, BinIcon } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation';
const DGsRows = Row.extend`
margin-top: ${remcalc(-7)};
`;
const Box = styled.div`
position: relative;
text-decoration: none;
color: ${props => props.theme.secondary};
background-color: ${props => props.theme.white};
box-shadow: 0 ${remcalc(2)} 0 0 rgba(0, 0, 0, 0.05);
border: solid ${remcalc(1)} ${props => props.theme.grey};
margin-top: ${remcalc(20)};
margin-bottom: 0;
padding: ${remcalc(18)};
min-height: ${remcalc(258)};
display: flex;
flex-direction: column;
&:last-child {
margin-bottom: ${remcalc(20)};
}
`;
const BoxCreate = Box.extend`
background-color: ${props => props.theme.disabled};
&:hover {
background-color: ${props => props.theme.white};
}
`;
const Oval = styled.div`
border: solid ${remcalc(1)} ${props => props.theme.grey};
border-radius: 50%;
width: ${remcalc(48)};
height: ${remcalc(48)};
line-height: ${remcalc(48)};
font-size: ${remcalc(24)};
text-align: center;
margin-bottom: ${remcalc(20)};
`;
const CreateTitle = Small.extend`
font-weight: 600;
text-align: center;
`;
const ServiceTitle = H3.extend`
margin-top: ${remcalc(10)};
font-weight: 600;
`;
const StyledLink = styled(Link)`
display: flex;
flex-grow: 1;
text-decoration: none;
color: ${props => props.theme.secondary};
`;
const StyledCreateLink = styled(StyledLink)`
flex-direction: column;
justify-content: center;
align-items: center;
align-content: center;
display: flex;
`;
const StyledIconButton = styled(IconButton)`
position: absolute;
right: 0;
bottom: 0;
border: none;
&:hover,
&:focus,
&:active,
&:active:hover,
&:active:focus {
background-color: ${props => props.theme.white};
}
&:focus > svg,
&:hover > svg {
fill: ${props => props.theme.red};
}
&:active > svg,
&:active:hover > svg,
&:active:focus > svg {
fill: ${props => props.theme.redDark};
}
`;
export const DeploymentGroupList = ({
deploymentGroups,
importable,
loading,
error,
match
}) => {
const _title = <Title>Deployment groups</Title>;
if (loading && (!deploymentGroups || !deploymentGroups.length)) {
return (
<LayoutContainer center>
<Loader />
</LayoutContainer>
);
}
const _error =
error && (!deploymentGroups || !deploymentGroups.length) ? (
<ErrorMessage
title="Ooops!"
message="An error occurred while loading your deployment groups."
/>
) : null;
const groups = forceArray(deploymentGroups).map(({ slug, name }) => (
<Col xs={12} sm={4} md={3} lg={3} key={slug}>
<Box>
<StyledLink to={`${match.path}/${slug}`}>
<ServiceTitle>{name}</ServiceTitle>
</StyledLink>
<StyledIconButton to={`${match.url}/${slug}/delete`}>
<BinIcon />
</StyledIconButton>
</Box>
</Col>
));
const create = [
<Col xs={12} sm={4} md={3} lg={3} key="~create">
<BoxCreate>
<StyledCreateLink to={`${match.path}/~create`}>
<Oval>+</Oval>
<CreateTitle>Create new deployment group</CreateTitle>
</StyledCreateLink>
</BoxCreate>
</Col>
].concat(
forceArray(importable).map(({ slug, name }) => (
<Col xs={12} sm={4} md={3} lg={3} key={slug}>
<BoxCreate>
<StyledCreateLink to={`${match.path}/~import/${slug}`}>
<Oval>&#10549;</Oval>
<CreateTitle>{name}</CreateTitle>
</StyledCreateLink>
</BoxCreate>
</Col>
))
);
return (
<LayoutContainer>
{_title}
{_error}
<DGsRows>
{groups}
{create}
</DGsRows>
</LayoutContainer>
);
};
DeploymentGroupList.propTypes = {
deploymentGroups: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string
})
)
};
export default compose(
graphql(DeploymentGroupsQuery, {
options: {
pollInterval: 1000
},
props: ({ data: { deploymentGroups, loading, error } }) => ({
deploymentGroups:
deploymentGroups && deploymentGroups.length
? deploymentGroups.filter(dg => dg.status !== 'DELETED')
: null,
loading,
error
})
}),
graphql(DeploymentGroupsImportableQuery, {
props: ({ data: { importableDeploymentGroups } }) => ({
importable: importableDeploymentGroups
})
}),
withNotFound([GqlPaths.DEPLOYMENT_GROUP])
)(DeploymentGroupList);

View File

@ -1,64 +0,0 @@
import React from 'react';
import { compose, graphql } from 'react-apollo';
import get from 'lodash.get';
import ManifestQuery from '@graphql/Manifest.gql';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { Loader, ErrorMessage } from '@components/messaging';
import { Environment } from '@components/manifest';
const EnvironmentReadOnly = ({
files = [],
environment = '',
loading,
error
}) => {
const _title = <Title>Environment</Title>;
if (loading && !environment.length && !files.length) {
return (
<LayoutContainer center>
{_title}
<Loader />
</LayoutContainer>
);
}
if (error) {
return (
<LayoutContainer>
{_title}
<ErrorMessage
title="Ooops!"
message="An error occurred while loading environment data."
/>
</LayoutContainer>
);
}
return (
<LayoutContainer>
{_title}
<Environment defaultValue={environment} files={files} readOnly />
</LayoutContainer>
);
};
export default compose(
graphql(ManifestQuery, {
options: props => ({
fetchPolicy: 'cache-and-network',
variables: {
deploymentGroupSlug: props.match.params.deploymentGroup
}
}),
props: ({ data: { deploymentGroup, loading, error } }) => ({
files: get(deploymentGroup, 'version.manifest.files', []),
environment: get(deploymentGroup, 'version.manifest.environment', ''),
loading,
error
})
})
)(EnvironmentReadOnly);

View File

@ -1,2 +0,0 @@
export { default as InstanceList } from './list';
export { default as InstancesTooltip } from './tooltip';

View File

@ -1,151 +0,0 @@
import React from 'react';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import InstancesQuery from '@graphql/Instances.gql';
import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { Loader, ErrorMessage } from '@components/messaging';
import { InstanceListItem, EmptyInstances } from '@components/instances';
import { toggleInstancesTooltip } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
export const InstanceList = ({
deploymentGroup,
instances = [],
loading,
error,
instancesTooltip,
toggleInstancesTooltip
}) => {
const _title = <Title>Instances</Title>;
if (loading && !forceArray(instances).length) {
return (
<LayoutContainer center>
{_title}
<Loader />
</LayoutContainer>
);
}
if (error) {
return (
<LayoutContainer>
{_title}
<ErrorMessage
title="Ooops!"
message="An error occurred while loading your instances."
/>
</LayoutContainer>
);
}
if (deploymentGroup.status === 'PROVISIONING' && !instances.length) {
return (
<LayoutContainer center>
{_title}
<Loader msg="Just a moment, were on it" />
</LayoutContainer>
);
}
const handleHealthMouseOver = (evt, instance) => {
handleMouseOver(evt, instance, 'healthy');
};
const handleStatusMouseOver = (evt, instance) => {
handleMouseOver(evt, instance, 'status');
};
const handleMouseOver = (evt, instance, type) => {
const label = evt.currentTarget;
const labelRect = label.getBoundingClientRect();
const offset = type === 'healthy' ? 48 : type === 'status' ? 36 : 0;
const position = {
left: `${window.scrollX + labelRect.left + offset}px`,
top: `${window.scrollY + labelRect.bottom}px`
};
const tooltipData = {
instance,
position,
type
};
toggleInstancesTooltip(tooltipData);
};
const handleMouseOut = evt => {
toggleInstancesTooltip({ show: false });
};
const instanceList = instances.map((instance, index) => (
<InstanceListItem
instance={instance}
key={instance.id}
onHealthMouseOver={handleHealthMouseOver}
onStatusMouseOver={handleStatusMouseOver}
onMouseOut={handleMouseOut}
/>
));
const _instances = !instanceList.length ? <EmptyInstances /> : instanceList;
return (
<LayoutContainer>
{_title}
{_instances}
</LayoutContainer>
);
};
const mapStateToProps = (state, ownProps) => ({
instancesTooltip: state.ui.instances.tooltip
});
const mapDispatchToProps = dispatch => ({
toggleInstancesTooltip: data => dispatch(toggleInstancesTooltip(data))
});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
const InstanceListGql = graphql(InstancesQuery, {
options(props) {
const params = props.match.params;
const deploymentGroupSlug = params.deploymentGroup;
const serviceSlug = params.service;
return {
pollInterval: 1000,
variables: {
deploymentGroupSlug,
serviceSlug
}
};
},
props: ({ data: { deploymentGroup, loading, error } }) => ({
deploymentGroup,
instances: sortBy(
forceArray(
deploymentGroup &&
forceArray(deploymentGroup.services).reduce(
(instances, service) => instances.concat(service.instances),
[]
)
).filter(Boolean),
['name']
),
loading,
error
})
});
export default compose(
UiConnect,
InstanceListGql,
withNotFound([GqlPaths.DEPLOYMENT_GROUP, GqlPaths.SERVICES])
)(InstanceList);

View File

@ -1,64 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { Tooltip, TooltipLabel } from 'joyent-ui-toolkit';
const StyledContainer = styled.div`
position: absolute;
top: 0;
left: 0;
`;
const healthMessages = {
healthy: 'Your instance is operating as expected',
unhealthy: 'Your instance is not operating as expected',
maintenance:
"You've set your instance to this manually, use the Container Pilot CLI to change",
unknown: "We've connected to your instance but we have no health information",
unavailable: 'We cannot connect to your instance'
};
const statusMessages = {
running: 'Your instance is operating',
provisioning: 'Your instance is downloading dependencies and compiling',
ready:
"Your instance finished provisioning and is ready to be run, it'll be running soon",
stopping: 'Your instance is going to be stopped soon',
stopped: "Your instance isn't doing anything, you can start it",
offline: 'We have no idea what this means, do you??????',
failed: 'Your instance has crashed',
unknown: 'We cannot work out what status your instance is in'
};
export const InstancesTooltip = ({ instancesTooltip }) => {
if (instancesTooltip.show) {
const { type, instance } = instancesTooltip;
const message =
type === 'healthy'
? healthMessages[(instance.healthy || '').toLowerCase()]
: type === 'status'
? statusMessages[(instance.status || '').toLowerCase()]
: '';
return (
<StyledContainer>
<Tooltip {...instancesTooltip.position} secondary>
<TooltipLabel>{message}</TooltipLabel>
</Tooltip>
</StyledContainer>
);
}
return null;
};
const mapStateToProps = (state, ownProps) => ({
instancesTooltip: state.ui.instances.tooltip
});
const mapDispatchToProps = dispatch => ({});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
export default UiConnect(InstancesTooltip);

View File

@ -1,513 +0,0 @@
import React, { Component } from 'react';
import { reduxForm } from 'redux-form';
import { compose, graphql } from 'react-apollo';
import { withRouter } from 'react-router';
import { Redirect } from 'react-router-dom';
import intercept from 'apr-intercept';
import paramCase from 'param-case';
import get from 'lodash.get';
import remove from 'lodash.remove';
import flatten from 'lodash.flatten';
import uniq from 'lodash.uniq';
import find from 'lodash.find';
import { safeLoad } from 'js-yaml';
import uuid from 'uuid/v4';
import forceArray from 'force-array';
import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
import DeploymentGroupCreateMutation from '@graphql/DeploymentGroupCreate.gql';
import DeploymentGroupProvisionMutation from '@graphql/DeploymentGroupProvision.gql';
import DeploymentGroupConfigQuery from '@graphql/DeploymentGroupConfig.gql';
import PortalQuery from '@graphql/Portal.gql';
import { client } from '@state/store';
import { ErrorMessage } from '@components/messaging';
import { Environment, Name, Review, Manifest } from '@components/manifest';
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
const CNS_PRIVATE = 'TRITON_CNS_SEARCH_DOMAIN_PRIVATE';
const CNS_PUBLIC = 'TRITON_CNS_SEARCH_DOMAIN_PUBLIC';
// TODO: move state to redux. why: because in redux we can cache transactional
// state between refreshes
class DeploymentGroupEditOrCreate extends Component {
constructor(props) {
super(props);
const { create, files = [], manifest } = props;
const type = create ? 'create' : 'edit';
const NameForm =
create &&
reduxForm({
form: `${type}-deployment-group`,
destroyOnUnmount: true,
forceUnregisterOnUnmount: true,
asyncValidate: async ({ name = '' }) => {
const [err, res] = await intercept(
client.query({
fetchPolicy: 'network-only',
query: DeploymentGroupBySlugQuery,
variables: {
slug: paramCase(name.trim())
}
})
);
if (err) {
return;
}
if (!res.data.deploymentGroups.length) {
return;
}
// eslint-disable-next-line no-throw-literal
throw { name: `"${name}" already exists!` };
}
})(Name);
const ManifestForm = reduxForm({
form: `${type}-deployment-group`
})(Manifest);
const ReviewForm = reduxForm({
form: `${type}-deployment-group`
})(Review);
this.state = {
type,
defaultStage: create ? 'name' : 'edit',
manifestStage: create ? 'manifest' : 'edit',
name: '',
manifest: '',
environment: '',
files: this.resolveManifestFiles(files, manifest),
services: [],
environmentToggles: {},
loading: false,
error: null,
NameForm,
ReviewForm,
ManifestForm
};
this.state.EnvironmentForm = this.getEnvironmentForm(
this.state.files,
manifest
);
this.stages = {
name: create && this.renderNameForm.bind(this),
[create ? 'manifest' : 'edit']: this.renderManifestEditor.bind(this),
environment: this.renderEnvironmentEditor.bind(this),
review: this.renderReview.bind(this)
};
this.handleNameSubmit =
type === 'create' && this.handleNameSubmit.bind(this);
this.handleManifestSubmit = this.handleManifestSubmit.bind(this);
this.handleEnvironmentSubmit = this.handleEnvironmentSubmit.bind(this);
this.handleReviewSubmit = this.handleReviewSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleFileAdd = this.handleFileAdd.bind(this);
this.handleRemoveFile = this.handleRemoveFile.bind(this);
this.handleEnvironmentToggle = this.handleEnvironmentToggle.bind(this);
}
resolveManifestFiles(currentFiles = [], manifestStr = '') {
if (!manifestStr.length) {
return [];
}
let manifest = {};
try {
manifest = safeLoad(manifestStr);
} catch (err) {
console.error(err);
return [];
}
const services = manifest.services ? manifest.services : manifest;
const filenames = uniq(
// eslint-disable-next-line camelcase
flatten(Object.values(services).map(({ env_file }) => env_file))
);
return filenames
.filter(Boolean)
.filter(filename => !find(currentFiles, ['name', filename]))
.map(this.getDefaultFile)
.concat(currentFiles);
}
getEnvironmentForm(files = [], manifest = '') {
const { type } = this.state;
const initialValues = files.reduce(
(acc, { id, name, value }) =>
Object.assign(acc, {
[`file-name-${id}`]: name,
[`file-value-${id}`]: value
}),
{}
);
return reduxForm({
form: `${type}-deployment-group`,
initialValues
})(Environment);
}
getEnvironmentDefaultValue() {
const { environment = '' } = this.props;
const { manifest = '' } = this.state;
if (environment.length) {
return environment;
}
const searchDomain = [
`${CNS_PRIVATE}=${this.props.tritonId}.${this.props
.dataCenter}.cns.joyent.com`,
`${CNS_PUBLIC}=${this.props.tritonId}.${this.props
.dataCenter}.triton.zone`
].join('\n');
const names = forceArray(manifest.match(INTERPOLATE_REGEX))
.map(name => name.replace(/^\$/, ''))
.filter(name => [CNS_PRIVATE, CNS_PUBLIC].indexOf(name) < 0);
const vars = uniq(names)
.map(name => `\n${name}=`)
.join('');
return `${searchDomain}\n\n# define your interpolatable variables here\n${vars}`;
}
getDefaultFile(name = '') {
return {
id: uuid(),
name,
value: '# define your environment variables here\n'
};
}
createDeploymentGroup = async () => {
const { createDeploymentGroup, deploymentGroup, edit } = this.props;
if (edit && (!deploymentGroup || !deploymentGroup.id)) {
this.setState({
error: 'Unexpected Error: Inexistent DeploymentGroup!'
});
return {};
}
if (deploymentGroup && deploymentGroup.id) {
return deploymentGroup;
}
const { name } = this.state;
const [err, res] = await intercept(createDeploymentGroup({ name }));
if (err) {
this.setState({
error: err.message
});
}
return err ? {} : res.data.createDeploymentGroup;
};
provision = async deploymentGroupId => {
const { manifest, environment, files } = this.state;
const { provisionManifest } = this.props;
const [err] = await intercept(
provisionManifest({
deploymentGroupId,
type: 'COMPOSE',
format: 'YAML',
environment: environment || '',
files,
raw: manifest
})
);
if (err) {
this.setState({
error: err.message
});
}
return err ? null : true;
};
handleNameSubmit({ name = '' }) {
this.setState({ name }, () =>
this.redirect({ stage: 'manifest', prog: true })
);
}
handleManifestSubmit({ manifest = '' }) {
const { files } = this.state;
const _manifest = manifest || this.props.manifest;
const _files = this.resolveManifestFiles(files, _manifest);
const EnvironmentForm = this.getEnvironmentForm(_files, _manifest);
this.setState(
{ manifest: _manifest, EnvironmentForm, files: _files },
() => {
this.redirect({ stage: 'environment', prog: true });
}
);
}
handleEnvironmentSubmit(change) {
const { environment = '' } = change;
const { name, manifest } = this.state;
const files = Object.values(
Object.keys(change).reduce((acc, key) => {
const match = key.match(/file-(name|value)-(.*)/);
if (!match) {
return acc;
}
const [_, type, id] = match;
if (!acc[id]) {
acc[id] = {
id
};
}
acc[id][type] = change[key];
return acc;
}, {})
);
const getConfig = async () => {
const { environment } = this.state;
const [err, conf] = await intercept(
client.query({
query: DeploymentGroupConfigQuery,
fetchPolicy: 'network-only',
variables: {
deploymentGroupName: name,
type: 'COMPOSE',
format: 'YAML',
environment: environment || '',
files,
raw: manifest
}
})
);
if (err) {
return this.setState({
error: err.message
});
}
const { data } = conf;
const { config: services } = data;
this.setState({ loading: false, services, files }, () => {
this.redirect({ stage: 'review', prog: true });
});
};
this.setState(
{
environment: environment || this.getEnvironmentDefaultValue(),
loading: true
},
getConfig
);
}
handleReviewSubmit() {
const { history } = this.props;
const submit = async () => {
const { id, slug } = await this.createDeploymentGroup();
if (!id) {
return;
}
const manifest = await this.provision(id);
if (!manifest) {
return;
}
history.push(`/deployment-groups/${slug}`);
};
this.setState({ loading: true }, submit);
}
handleCancel() {
const { history, create, deploymentGroup } = this.props;
history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`);
return false;
}
handleFileAdd() {
const { files = [] } = this.state;
this.setState({
files: files.concat([this.getDefaultFile()])
});
}
handleRemoveFile(fileId) {
const { files = [] } = this.state;
this.setState({
files: remove(files, ({ id }) => id !== fileId)
});
}
handleEnvironmentToggle(serviceName) {
const { environmentToggles } = this.state;
this.setState({
environmentToggles: Object.assign({}, environmentToggles, {
[serviceName]: !environmentToggles[serviceName]
})
});
}
redirect({ stage = 'name', prog = false }) {
const { match, history, create } = this.props;
const regex = create ? /\/~create(.*)/ : /\/manifest(.*)/;
const to = match.url.replace(
regex,
create ? `/~create/${stage}` : `/manifest/${stage}`
);
if (!prog) {
return <Redirect to={to} />;
}
history.push(to);
}
renderNameForm() {
const { NameForm } = this.state;
const { dataCenter } = this.props;
return (
<NameForm
dataCenter={dataCenter}
onSubmit={this.handleNameSubmit}
onCancel={this.handleCancel}
/>
);
}
renderManifestEditor() {
const { ManifestForm } = this.state;
return (
<ManifestForm
defaultValue={this.props.manifest}
onSubmit={this.handleManifestSubmit}
onCancel={this.handleCancel}
/>
);
}
renderEnvironmentEditor() {
const { EnvironmentForm, files, loading } = this.state;
return (
<EnvironmentForm
defaultValue={this.getEnvironmentDefaultValue()}
files={files}
onSubmit={this.handleEnvironmentSubmit}
onCancel={this.handleCancel}
onAddFile={this.handleFileAdd}
onRemoveFile={this.handleRemoveFile}
loading={loading}
/>
);
}
renderReview() {
const { ReviewForm, environmentToggles } = this.state;
return (
<ReviewForm
onSubmit={this.handleReviewSubmit}
onCancel={this.handleCancel}
onEnvironmentToggle={this.handleEnvironmentToggle}
environmentToggles={environmentToggles}
{...this.state}
/>
);
}
render() {
const { error, defaultStage, manifestStage, manifest, name } = this.state;
if (error) {
return <ErrorMessage title="Ooops!" message={error} />;
}
const { match, create } = this.props;
const stage = match.params.stage;
if (!stage) {
return this.redirect({ stage: defaultStage });
}
if (!this.stages[stage]) {
return this.redirect({ stage: defaultStage });
}
if (create && stage !== 'name' && !name) {
return this.redirect({ stage: defaultStage });
}
if (stage === 'environment' && !manifest) {
return this.redirect({ stage: manifestStage });
}
return this.stages[stage]();
}
}
export default compose(
graphql(PortalQuery, {
props: ({ data: { portal = {} } }) => ({
dataCenter: get(portal, 'datacenter.region', ''),
tritonId: get(portal, 'user.tritonId', '')
})
}),
graphql(DeploymentGroupCreateMutation, {
props: ({ mutate }) => ({
createDeploymentGroup: variables => mutate({ variables })
})
}),
graphql(DeploymentGroupProvisionMutation, {
props: ({ mutate }) => ({
provisionManifest: variables => mutate({ variables })
})
})
)(withRouter(DeploymentGroupEditOrCreate));

View File

@ -1,118 +0,0 @@
import React from 'react';
import { compose, graphql } from 'react-apollo';
import get from 'lodash.get';
import forceArray from 'force-array';
import ManifestQuery from '@graphql/Manifest.gql';
import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
import ManifestEditOrCreate from '@containers/manifest/edit-or-create';
import { Progress } from '@components/manifest';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { Loader, ErrorMessage, WarningMessage } from '@components/messaging';
const Manifest = ({
loading,
error,
manifest = '',
environment = '',
files = [],
deploymentGroup = null,
hasManifest = false,
match
}) => {
const stage = match.params.stage;
const _title = <Title>Edit Manifest</Title>;
if (
loading ||
!deploymentGroup ||
(!hasManifest && !deploymentGroup.imported)
) {
return (
<LayoutContainer center>
{_title}
<Loader />
</LayoutContainer>
);
}
if (error) {
return (
<LayoutContainer>
{_title}
<ErrorMessage
title="Ooops!"
message="An error occurred while loading your deployment group."
/>
</LayoutContainer>
);
}
const _notice =
deploymentGroup && deploymentGroup.imported && !manifest ? (
<WarningMessage
title="Be aware"
message="Since this Deployment Group was imported, it doesn&#x27;t have the initial manifest."
/>
) : null;
return (
<LayoutContainer>
{_title}
<Progress stage={stage} edit />
{_notice}
<ManifestEditOrCreate
manifest={manifest}
environment={environment}
files={files}
deploymentGroup={deploymentGroup}
edit
/>
</LayoutContainer>
);
};
export default compose(
graphql(ManifestQuery, {
options: props => ({
fetchPolicy: 'network-only',
variables: {
deploymentGroupSlug: props.match.params.deploymentGroup
}
}),
props: ({ data: { deploymentGroup, loading, error } }) => ({
files: get(deploymentGroup, 'version.manifest.files', []),
manifest: get(deploymentGroup, 'version.manifest.raw', ''),
environment: get(deploymentGroup, 'version.manifest.environment', ''),
hasManifest: Boolean(get(deploymentGroup, 'version.manifest')),
loading,
error
})
}),
graphql(DeploymentGroupBySlugQuery, {
options: props => ({
variables: {
slug: props.match.params.deploymentGroup
}
}),
props: ({
data: { deploymentGroups, loading, error, startPolling, stopPolling }
}) => {
const dgs = forceArray(deploymentGroups);
if (!dgs.length) {
startPolling(1000);
} else {
stopPolling();
}
return {
deploymentGroup: dgs[0],
loading,
error
};
}
})
)(Manifest);

View File

@ -1 +0,0 @@
export * from './metrics-data-hoc';

View File

@ -1,160 +0,0 @@
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import find from 'lodash.find';
import uniqBy from 'lodash.uniqby';
import get from 'lodash.get';
import moment from 'moment';
export const MetricNames = [
'AVG_MEM_BYTES',
'AVG_LOAD_PERCENT',
'AGG_NETWORK_BYTES'
];
export const withServiceMetricsPolling = ({
pollingInterval = 1000, // In milliseconds
getPreviousEnd = () =>
moment()
.utc()
.format()
}) => {
return WrappedComponent => {
return class extends Component {
componentDidMount() {
this._poll = setInterval(() => {
const { loading, fetchMoreMetrics } = this.props;
const previousEnd = getPreviousEnd(this.props);
if (!loading && previousEnd) {
fetchMoreMetrics(previousEnd);
}
}, pollingInterval); // TODO this is the polling interval - think about amount is the todo I guess...
}
componentWillUnmount() {
clearInterval(this._poll);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
};
export const withServiceMetricsGql = ({
gqlQuery,
graphDurationSeconds,
updateIntervalSeconds,
variables = () => ({}),
props = () => ({})
}) => {
const getPreviousMetrics = (
previousResult,
serviceId,
instanceId,
metricName
) => {
const services = get(previousResult, 'deploymentGroup.services', []);
if (!services.length) {
return [];
}
const service = find(services, ['id', serviceId]);
if (!service) {
return [];
}
const instance = find(service.instances, ['id', instanceId]);
if (!instance) {
return [];
}
const metrics = find(instance.metrics, ['name', metricName]);
if (!metrics) {
return [];
}
return get(metrics, 'metrics', []);
};
const getNextResult = (previousResult, fetchNextResult) => {
const deploymentGroup = fetchNextResult.deploymentGroup;
return {
deploymentGroup: {
...deploymentGroup,
services: deploymentGroup.services.map(service => ({
...service,
instances: service.instances.map(instance => ({
...instance,
metrics: instance.metrics.map(metric => ({
...metric,
metrics: uniqBy(
getPreviousMetrics(
previousResult,
service.id,
instance.id,
metric.name
).concat(metric.metrics),
'time'
)
}))
}))
}))
}
};
};
return graphql(gqlQuery, {
options(props) {
const params = props.match.params;
const deploymentGroupSlug = params.deploymentGroup;
// This is potentially prone to overfetching if we already have data within timeframe and we leave the page then come back to it
const end = moment();
const start = moment(end).subtract(
graphDurationSeconds + updateIntervalSeconds,
'seconds'
); // TODO initial amount of data we wanna get - should be the same as what we display + 15 secs
return {
variables: {
deploymentGroupSlug,
metricNames: MetricNames,
start: start.utc().format(),
end: end.utc().format(),
...variables(props)
}
};
},
props: ({ data: { variables, fetchMore, ...rest } }) => {
const fetchMoreMetrics = previousEnd => {
fetchMore({
variables: {
...variables,
start: previousEnd,
end: moment()
.utc()
.format()
},
updateQuery: (
previousResult,
{ fetchMoreResult, queryVariables }
) => {
return getNextResult(previousResult, fetchMoreResult);
}
});
};
return {
fetchMoreMetrics,
...props(rest)
};
}
});
};

View File

@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'react-apollo';
import { Breadcrumb as BreadcrumbComponent } from '@components/navigation';
import withNotFound from './not-found-hoc';
import {
deploymentGroupBySlugSelector,
serviceBySlugSelector
} from '@root/state/selectors';
export const Breadcrumb = ({
deploymentGroup,
service,
location: { pathname }
}) => {
const path = pathname.split('/');
const links = [
{
name: 'Dashboard',
pathname: '/'
}
];
if (deploymentGroup) {
links.push({
name: deploymentGroup.name,
pathname: path.slice(0, 3).join('/')
});
}
if (service) {
links.push({
name: service.name,
pathname: path.slice(0, 5).join('/')
});
}
return <BreadcrumbComponent links={links} />;
};
Breadcrumb.propTypes = {
deploymentGroup: PropTypes.object,
service: PropTypes.object,
location: PropTypes.object
};
const connectBreadcrumb = connect(
(state, ownProps) => {
const params = ownProps.match.params;
const deploymentGroupSlug = params.deploymentGroup;
const serviceSlug = params.service;
return {
deploymentGroup: deploymentGroupBySlugSelector(deploymentGroupSlug)(
state
),
service: serviceBySlugSelector(serviceSlug)(state),
location: ownProps.location
};
},
dispatch => ({})
);
export default compose(connectBreadcrumb, withNotFound())(Breadcrumb);

View File

@ -1,19 +0,0 @@
import React from 'react';
import { graphql } from 'react-apollo';
import get from 'lodash.get';
import PortalQuery from '@graphql/Portal.gql';
import { Header as HeaderComponent } from '@components/navigation';
export const Header = ({ datacenter, username }) => (
<HeaderComponent datacenter={datacenter} username={username} />
);
const HeaderWithData = graphql(PortalQuery, {
props: ({ data: { portal = {} } }) => ({
datacenter: get(portal, 'datacenter.region', ''),
username: get(portal, 'user.firstName', '')
})
})(Header);
export default HeaderWithData;

View File

@ -1,4 +0,0 @@
export { default as Header } from './header';
export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';
export { default as withNotFound, GqlPaths } from './not-found-hoc';

View File

@ -1,42 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'react-apollo';
import withNotFound from './not-found-hoc';
import { Menu as MenuComponent } from '@components/navigation';
export const Menu = ({ location, match, sections }) => {
if (!sections || !sections.length) {
return null;
}
const sectionsWithPathnames = sections.map(section => {
return {
name: section.name,
pathname: `${match.url}/${section.pathname}`
};
});
return <MenuComponent links={sectionsWithPathnames} />;
};
const connectMenu = connect(
(state, ownProps) => {
const params = ownProps.match.params;
const deploymentGroupSlug = params.deploymentGroup;
const serviceSlug = params.service;
if ((deploymentGroupSlug || '').match(/^~/)) {
return {};
}
const sections = serviceSlug
? state.ui.sections.services
: deploymentGroupSlug ? state.ui.sections.deploymentGroups : null;
return {
sections
};
},
dispatch => ({})
);
export default compose(connectMenu, withNotFound())(Menu);

View File

@ -1,69 +0,0 @@
import React, { Component } from 'react';
import { NotFound } from '@components/navigation';
export const GqlPaths = {
DEPLOYMENT_GROUP: 'deploymentGroup',
SERVICES: 'services'
};
export default paths => {
return WrappedComponent => {
return class extends Component {
componentWillReceiveProps(nextProps) {
if (paths) {
const { error, location, history } = nextProps;
if (
error &&
(!location.state || !location.state.notFound) &&
(error.graphQLErrors && error.graphQLErrors.length)
) {
const graphQLError = error.graphQLErrors[0];
if (graphQLError.message === 'Not Found') {
const notFound = graphQLError.path.pop();
if (paths.indexOf(notFound) > -1) {
history.replace(location.pathname, { notFound });
}
}
}
}
}
render() {
const { location, match } = this.props;
if (location.state && location.state.notFound) {
const notFound = location.state.notFound;
if (paths && paths.indexOf(notFound) > -1) {
let title;
let to;
let link;
if (notFound === 'services' || notFound === 'service') {
title = 'This service doesnt exist';
to = match.url
.split('/')
.slice(0, 3)
.join('/');
link = 'Back to services';
} else if (notFound === 'deploymentGroup') {
title = 'This deployment group doesnt exist';
to = '/deployment-group';
link = 'Back to dashboard';
}
return (
<NotFound
title={title}
message="Sorry, but our princess is in another castle."
to={to}
link={link}
/>
);
}
return null;
}
return <WrappedComponent {...this.props} />;
}
};
};
};

Some files were not shown because too many files have changed in this diff Show More