chore: remove copilot packages (#680)
This commit is contained in:
parent
1fb157240c
commit
e842f73b41
20
CHANGELOG.md
20
CHANGELOG.md
@ -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))
|
||||
|
||||
|
||||
|
@ -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.
|
||||
```
|
61
README.md
61
README.md
@ -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)
|
||||
[![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)
|
||||
@ -21,64 +18,6 @@
|
||||
|
||||
## 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
|
||||
|
||||
See [the contribute file](CONTRIBUTING.md)!
|
||||
|
BIN
copilot.png
BIN
copilot.png
Binary file not shown.
Before Width: | Height: | Size: 7.9 KiB |
@ -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
|
@ -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"]
|
@ -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
|
@ -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
|
142
docker/api/bootstrap-data.js
vendored
142
docker/api/bootstrap-data.js
vendored
@ -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();
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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();
|
@ -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"]
|
@ -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
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"]
|
@ -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
|
@ -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
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
40
gen-keys.sh
40
gen-keys.sh
@ -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'"
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "container-pilot-dashboard",
|
||||
"name": "joyent-portal",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "MPL-2.0",
|
||||
@ -14,7 +14,7 @@
|
||||
"lint-license": "./scripts/license-to-fail",
|
||||
"lint-docs": "./scripts/quality-docs",
|
||||
"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-ci:packages": "lerna run lint-ci",
|
||||
"lint:packages": "lerna run lint",
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"presets": "joyent-portal",
|
||||
"plugins": [
|
||||
"styled-components",
|
||||
["inline-react-svg", {
|
||||
"ignorePattern": "libre-franklin"
|
||||
}]
|
||||
]
|
||||
}
|
@ -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
|
@ -1,4 +0,0 @@
|
||||
.nyc_output
|
||||
coverage
|
||||
dist
|
||||
build
|
@ -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
|
||||
}
|
||||
}
|
18
packages/cp-frontend/.gitignore
vendored
18
packages/cp-frontend/.gitignore
vendored
@ -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*
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"syntax": "scss",
|
||||
"processors": ["stylelint-processor-styled-components"],
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-primer"]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -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"]
|
@ -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
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 |
@ -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>
|
@ -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;
|
||||
};
|
@ -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);
|
||||
})()
|
||||
);
|
@ -1,4 +0,0 @@
|
||||
const originalConfig = require('./webpack.config.dev.original');
|
||||
const patch = require('./patch-webpack-config');
|
||||
|
||||
module.exports = patch(originalConfig);
|
@ -1,4 +0,0 @@
|
||||
const originalConfig = require('./webpack.config.prod.original');
|
||||
const patch = require('./patch-webpack-config');
|
||||
|
||||
module.exports = patch(originalConfig);
|
@ -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 |
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default as DeploymentGroupDelete } from './delete';
|
@ -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>
|
||||
);
|
@ -1,2 +0,0 @@
|
||||
export { default as EmptyInstances } from './empty';
|
||||
export { default as InstanceListItem } from './list-item';
|
@ -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;
|
@ -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;
|
||||
`};
|
||||
`;
|
@ -1 +0,0 @@
|
||||
export { default as LayoutContainer } from './container';
|
@ -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}
|
||||
/>
|
||||
);
|
@ -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;
|
@ -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;
|
@ -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';
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
|
@ -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;
|
@ -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';
|
@ -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>
|
||||
);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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';
|
@ -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;
|
@ -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 can’t 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;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
export { default as ServiceScale } from './scale';
|
||||
export { default as ServiceDelete } from './delete';
|
||||
export { default as ServiceMetrics } from './metrics';
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
@ -1,3 +0,0 @@
|
||||
export { default as EmptyServices } from './empty';
|
||||
export { default as ServiceListItem } from './list-item';
|
||||
export { default as ServicesQuickActions } from './quick-actions';
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default as DeploymentGroupDelete } from './delete';
|
@ -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;
|
@ -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);
|
@ -1,3 +0,0 @@
|
||||
export { default as DeploymentGroupList } from './list';
|
||||
export { default as DeploymentGroupCreate } from './create';
|
||||
export { default as DeploymentGroupImport } from './import';
|
@ -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>⤵</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);
|
@ -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);
|
@ -1,2 +0,0 @@
|
||||
export { default as InstanceList } from './list';
|
||||
export { default as InstancesTooltip } from './tooltip';
|
@ -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, we’re 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);
|
@ -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);
|
@ -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));
|
@ -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'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);
|
@ -1 +0,0 @@
|
||||
export * from './metrics-data-hoc';
|
@ -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)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@ -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);
|
@ -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;
|
@ -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';
|
@ -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);
|
@ -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 doesn’t exist';
|
||||
to = match.url
|
||||
.split('/')
|
||||
.slice(0, 3)
|
||||
.join('/');
|
||||
link = 'Back to services';
|
||||
} else if (notFound === 'deploymentGroup') {
|
||||
title = 'This deployment group doesn’t 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
Loading…
Reference in New Issue
Block a user