Move docker-store docs to docker-store subdirectory

This commit is contained in:
Misty Stanley-Jones 2016-09-28 14:40:26 -07:00
parent 9b50ee3943
commit c493719f19
606 changed files with 0 additions and 49687 deletions

View File

@ -1,17 +0,0 @@
FROM node:6.0.0-slim
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
COPY npm-shrinkwrap.json /usr/src/app/
RUN npm install
ENV NODE_ENV production
COPY . /usr/src/app
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

8
Jenkinsfile vendored
View File

@ -1,8 +0,0 @@
// Only run on Linux atm
wrappedNode(label: 'docker') {
deleteDir()
stage "checkout"
checkout scm
documentationChecker("docs")
}

100
README.md
View File

@ -1,100 +0,0 @@
# Mercury
[![Circle CI](https://circleci.com/gh/docker/mercury-ui.svg?style=shield&circle-token=03609595272ae7f08f9b7d0d276f2dff340d8735)](https://circleci.com/gh/docker/mercury-ui) [![codecov.io](https://codecov.io/github/docker/mercury-ui/coverage.svg?branch=master&token=y0dAFR3ipK)](https://codecov.io/github/docker/mercury-ui?branch=master)
### Develop
- Download [Node](https://nodejs.org/en/) or install it via homebrew `brew install node`
- Install [React dev tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) and [Redux dev tools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
In order to test user actions you will need to provide JWT authentication.
Add your JWT to your environment variables keyed by `DOCKERSTORE_TOKEN`
If you have CSRF token, key it in your environment variables by `DOCKERSTORE_CSRF`
```
npm install
npm run start
```
### Dockerize
- Download [Docker for Mac or Windows](https://beta.docker.com/docs/)
```
docker build -t docker/mercury-ui .
docker run -p 3000:3000 docker/mercury-ui
```
### Deploy to staging & production
#### Setup
1. Ensure access to Staging and Production VPNs
2. Docker Infra adds your developer certificates to the store & hub aws teams.
3. Install pip: [instructions here](https://pip.pypa.io/en/stable/installing/)
3. Install hub-boss (globally for now):
```
git clone https://github.com/docker/saas-mega
cd saas-mega/tools/hub-boss
sudo pip install -r requirements.txt
sudo pip install -e .
```
4. Follow the [Pass Runbook](https://docker.atlassian.net/wiki/display/DI/Pass+Runbook) to install `pass`, `gpg-agent` and `pinentry`.
5. Set up Store TLS certificates to access the staging & production Docker Daemons (see [docs](https://docs.docker.com/engine/security/https/))
```
mkdir -p ~/.docker/certs
cd ~/.docker/certs
# Get ca cert & key from pass
pass show dev/teams/store/docker/store-ca-cert.pem > ca.pem
pass show dev/teams/store/docker/store-ca-key.pem > ca-key.pem
# Generate a client private key
openssl genrsa -out key.pem 4096
# Generate client CSR
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
# Sign the public key
echo extendedKeyUsage = clientAuth > extfile.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile.cnf
# Fix permissions
chmod -v 0400 key.pem
chmod -v 0444 ca.pem cert.pem
# cleanup
rm ca-key.pem
rm extfile.cnf
rm client.csr
rm ca.srl
```
#### Deploying to Staging
1. Bump the app version:
```
git checkout master
git pull
npm version major # commits change with new major version in package.json, creates new git tag e.g. <v6.0.0>
git push origin master # push bump version commit
git push origin --tags # pushes newly created git tag
```
2. Wait for the new version to [build on DockerHub](https://hub.docker.com/r/docker/mercury-ui/builds/) (e.g. `6.0.0`)
3. Connect to the Staging VPN
4. Deploy: `./tools/scripts/deploy_stage.sh <old version> <new version> # e.g: 5.0.0 6.0.0`
#### Deploying to Production
1. Connect to Production VPN
2. Alert the team that you're doing a production deploy
3. Deploy to production: `./tools/scripts/deploy_prod.sh <old version> <new version>`
#### Rolling-Back
1. Notify team you're doing a UI roll-back:
2. `./tools/scripts/deploy_prod.sh <current version> <previous stable version>`

View File

@ -1,15 +0,0 @@
machine:
services:
- docker
node:
version: 6.0.0
dependencies:
cache_directories:
- node_modules
test:
override:
- npm run test:coverage
post:
- npm run codecov

View File

@ -1,49 +0,0 @@
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { applyRouterMiddleware, Router, browserHistory } from 'react-router';
import useScroll from 'react-router-scroll';
import configureRoutes from 'routes';
import configureStore from 'store';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { internalRouterReady } from 'actions/internal';
// Add global css styles
import 'lib/css/global.css';
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const routes = configureRoutes(store);
// Some components use react-tap-event-plugin to listen for touch events.
// This dependency is temporary and will go away once react v1.0 is released.
// Until then, be sure to inject this plugin at the start of your app.
// http://material-ui.com/#/get-started/installation
require('react-tap-event-plugin')();
browserHistory.listen(() => {
analytics.page();
});
const muiTheme = getMuiTheme();
// useScroll ensures that the page scrolls to the top when you load a new route
// scroll behavior can be extended with a custom shouldUpdateScroll function
// see https://github.com/taion/react-router-scroll for more info
store.dispatch(internalRouterReady());
render((
<Provider store={store}>
<MuiThemeProvider muiTheme={muiTheme}>
<Router
history={browserHistory}
routes={routes}
render={applyRouterMiddleware(useScroll())}
/>
</MuiThemeProvider>
</Provider>
), document.getElementById('app'));

View File

@ -1,7 +0,0 @@
web:
publish_all_ports: True
restart_policy:
MaximumRetryCount: 0
Name: always
ports:
- 3000

View File

@ -1,7 +0,0 @@
web:
publish_all_ports: True
restart_policy:
MaximumRetryCount: 0
Name: always
ports:
- 3000

View File

@ -1,4 +0,0 @@
DEBUG: 'False'
SERVICE_NAME: 'mercury-ui'
RELEASE_STAGE: 'production'
NODE_ENV: 'production'

View File

@ -1,4 +0,0 @@
DEBUG: 'True'
SERVICE_NAME: 'mercury-ui'
RELEASE_STAGE: 'staging'
NODE_ENV: 'production'

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,16 +0,0 @@
require('./server/env');
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;

7923
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,143 +0,0 @@
{
"name": "mercury",
"version": "82.0.0",
"description": "Mercury",
"main": "index.js",
"private": true,
"author": "Docker Inc.",
"license": "UNLICENSED",
"engines": {
"node": "~6.0.0",
"npm": "~3.8.7"
},
"config": {
"lintDirs": "client common server tools"
},
"scripts": {
"build": "NODE_ENV=production webpack -p --progress --colors",
"codecov": "codecov -t $CODECOV_REPOSITORY_TOKEN",
"lint:css": "stylelint \"**/*.css\"",
"lint:html": "htmllint server/**/*.html client/**/*.html src/**/*.html",
"lint:js:fix": "eslint eslint src/components/common/AutocompleteSearchBar/index.js --fix",
"lint:js": "eslint $npm_package_config_lintDirs --fix",
"lint": "npm-run-all lint:*",
"postmerge": "npm run runner -- hooks/postmerge",
"precommit": "npm run runner -- hooks/precommit $npm_package_config_lintDirs",
"runner": "node tools/runner",
"shrinkwrap": "npm run runner -- shrinkwrap",
"start": "node server/index.js",
"test:coverage": "istanbul cover --report text --report lcov _mocha mocha.js src/**/*test.js -x mocha.js -x src/**/*test.js",
"test": "mocha mocha.js src/**/*test.js"
},
"dependencies": {
"app-module-path": "1.0.6",
"aws-sdk": "2.3.18",
"babel-core": "6.7.7",
"babel-eslint": "6.0.4",
"babel-loader": "6.2.4",
"babel-plugin-add-module-exports": "0.1.4",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-polyfill": "6.7.4",
"babel-preset-es2015": "6.6.0",
"babel-preset-react": "6.5.0",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.7.2",
"babel-runtime": "6.6.1",
"basic-auth": "1.0.4",
"chai": "3.5.0",
"chai-enzyme": "0.4.2",
"classnames": "2.2.4",
"codecov": "1.0.1",
"compression": "1.6.1",
"country-list": "0.0.3",
"css-loader": "0.23.1",
"css-modules-require-hook": "4.0.0",
"dom-scroll-into-view": "1.2.0",
"enzyme": "2.2.0",
"es6-promise": "3.2.1",
"eslint": "2.8.0",
"eslint-config-airbnb": "7.0.0",
"eslint-plugin-jsx-a11y": "0.6.2",
"eslint-plugin-react": "4.3.0",
"eventemitter3": "1.2.0",
"express": "4.13.4",
"extract-text-webpack-plugin": "1.0.1",
"fbjs": "0.8.1",
"file-loader": "0.8.5",
"github-markdown-css": "2.2.1",
"htmllint-cli": "0.0.4",
"istanbul": "1.0.0-alpha.2",
"jsdom": "8.4.1",
"json-loader": "0.5.4",
"lodash": "4.11.2",
"lost": "6.7.2",
"marked": "0.3.5",
"material-ui": "0.15.0",
"md5": "2.1.0",
"mocha": "2.4.5",
"mocha-lcov-reporter": "1.2.0",
"moment": "2.13.0",
"normalize.css": "4.1.1",
"normalizr": "2.0.1",
"npm": "3.8.7",
"npm-run-all": "1.8.0",
"nprogress": "0.2.0",
"numeral": "1.5.3",
"postcss-calc": "5.2.1",
"postcss-color-function": "2.0.1",
"postcss-cssnext": "2.5.2",
"postcss-custom-media": "5.0.1",
"postcss-each": "0.9.1",
"postcss-import": "8.1.0",
"postcss-loader": "0.8.2",
"postcss-media-minmax": "2.1.2",
"postcss-mixins": "4.0.1",
"postcss-nested": "1.0.0",
"postcss-simple-vars": "1.2.0",
"promisify-node": "0.4.0",
"proxy-middleware": "0.15.0",
"qs": "6.1.0",
"raw-loader": "0.5.1",
"rc-slider": "3.7.0",
"react": "15.1.0",
"react-addons-css-transition-group": "15.0.2",
"react-addons-test-utils": "15.0.2",
"react-cookie": "0.4.5",
"react-dom": "15.0.2",
"react-dropzone": "3.5.0",
"react-modal": "1.2.1",
"react-notification": "5.0.3",
"react-redux": "4.4.5",
"react-router": "2.4.0",
"react-router-scroll": "0.2.0",
"react-select": "1.0.0-beta12",
"react-tap-event-plugin": "1.0.0",
"redux": "3.5.2",
"redux-actions": "0.9.1",
"redux-devtools": "3.2.0",
"redux-form": "5.2.2",
"redux-logger": "2.6.1",
"redux-promise-middleware": "3.0.0",
"redux-thunk": "2.0.1",
"reselect": "2.5.1",
"sinon": "1.17.3",
"style-loader": "0.13.1",
"stylelint": "6.2.2",
"stylelint-config-standard": "6.0.0",
"superagent": "1.8.3",
"superagent-promise": "1.1.0",
"url-loader": "0.5.7",
"url-pattern": "1.0.1",
"uuid": "2.0.2",
"uuid-v4": "0.1.0",
"velocity-react": "1.1.5",
"webpack": "1.13.0",
"webpack-dev-middleware": "1.6.1",
"webpack-hot-middleware": "2.10.0"
},
"devDependencies": {},
"optionalDependencies": {
"husky": "0.11.3"
}
}

View File

@ -1,21 +0,0 @@
var path = require('path');
var fs = require('fs');
require('css-modules-require-hook')({
generateScopedName: '[name]__[local]___[hash:base64:5]',
});
require('app-module-path').addPath(path.join(__dirname, '..', 'src'));
require('app-module-path')
.addPath(path.join(__dirname, '..', 'src', 'components'));
require('app-module-path')
.addPath(path.join(__dirname, '..', 'src', 'lib', 'css'));
require('babel-register', {
plugins: ['add-module-exports'],
});
// Webpack loaders aren't used Server-side, so we need to handle .md files
require.extensions['.md'] = function (module, filename) {
// eslint-disable-next-line
module.exports = fs.readFileSync(filename, 'utf8');
};

View File

@ -1,2 +0,0 @@
require('./env');
require('./server');

View File

@ -1,173 +0,0 @@
/*
Based on code from: https://github.com/chimurai/http-proxy-middleware
Includes Fixes:
- Fixed bug where domains with `-` would not work when rewriting cookie headers
The MIT License (MIT)
Copyright (c) 2015 Steven Chim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/* eslint-disable */
var os = require('os');
var http = require('http');
var https = require('https');
var owns = {}.hasOwnProperty;
module.exports = function proxyMiddleware(options) {
//enable ability to quickly pass a url for shorthand setup
if(typeof options === 'string'){
options = require('url').parse(options);
}
var httpLib = options.protocol === 'https:' ? https : http;
var request = httpLib.request;
options = options || {};
options.hostname = options.hostname;
options.port = options.port;
options.pathname = options.pathname || '/';
return function (req, resp, next) {
var url = req.url;
// You can pass the route within the options, as well
if (typeof options.route === 'string') {
if (url === options.route) {
url = '';
} else if (url.slice(0, options.route.length) === options.route) {
url = url.slice(options.route.length);
} else {
return next();
}
}
//options for this request
var opts = extend({}, options);
if (url && url.charAt(0) === '?') { // prevent /api/resource/?offset=0
if (options.pathname.length > 1 && options.pathname.charAt(options.pathname.length - 1) === '/') {
opts.path = options.pathname.substring(0, options.pathname.length - 1) + url;
} else {
opts.path = options.pathname + url;
}
} else if (url) {
opts.path = slashJoin(options.pathname, url);
} else {
opts.path = options.pathname;
}
opts.method = req.method;
opts.headers = options.headers ? merge(req.headers, options.headers) : req.headers;
applyViaHeader(req.headers, opts, opts.headers);
if (!options.preserveHost) {
// Forwarding the host breaks dotcloud
delete opts.headers.host;
}
var myReq = request(opts, function (myRes) {
var statusCode = myRes.statusCode
, headers = myRes.headers
, location = headers.location;
// Fix the location
if (((statusCode > 300 && statusCode < 304) || statusCode === 201) && location && location.indexOf(options.href) > -1) {
// absoulte path
headers.location = location.replace(options.href, slashJoin('/', slashJoin((options.route || ''), '')));
}
applyViaHeader(myRes.headers, opts, myRes.headers);
rewriteCookieHosts(myRes.headers, opts, myRes.headers, req);
resp.writeHead(myRes.statusCode, myRes.headers);
myRes.on('error', function (err) {
next(err);
});
myRes.pipe(resp);
});
myReq.on('error', function (err) {
next(err);
});
if (!req.readable) {
myReq.end();
} else {
req.pipe(myReq);
}
};
};
function applyViaHeader(existingHeaders, opts, applyTo) {
if (!opts.via) return;
var viaName = (true === opts.via) ? os.hostname() : opts.via;
var viaHeader = '1.1 ' + viaName;
if(existingHeaders.via) {
viaHeader = existingHeaders.via + ', ' + viaHeader;
}
applyTo.via = viaHeader;
}
function rewriteCookieHosts(existingHeaders, opts, applyTo, req) {
if (!opts.cookieRewrite || !owns.call(existingHeaders, 'set-cookie')) {
return;
}
var existingCookies = existingHeaders['set-cookie'],
rewrittenCookies = [],
rewriteHostname = (true === opts.cookieRewrite) ? os.hostname() : opts.cookieRewrite;
if (!Array.isArray(existingCookies)) {
existingCookies = [ existingCookies ];
}
for (var i = 0; i < existingCookies.length; i++) {
// Fixed bug where domains with `-` would not work when rewriting cookie headers
var rewrittenCookie = existingCookies[i].replace(/(Domain)=[a-z\.-_-]*?(;|$)/gi, '$1=' + rewriteHostname + '$2');
if (!req.connection.encrypted) {
rewrittenCookie = rewrittenCookie.replace(/;\s*?(Secure)/i, '');
}
rewrittenCookies.push(rewrittenCookie);
}
applyTo['set-cookie'] = rewrittenCookies;
}
function slashJoin(p1, p2) {
var trailing_slash = false;
if (p1.length && p1[p1.length - 1] === '/') { trailing_slash = true; }
if (trailing_slash && p2.length && p2[0] === '/') {p2 = p2.substring(1); }
return p1 + p2;
}
function extend(obj, src) {
for (var key in src) if (owns.call(src, key)) obj[key] = src[key];
return obj;
}
//merges data without changing state in either argument
function merge(src1, src2) {
var merged = {};
extend(merged, src1);
extend(merged, src2);
return merged;
}

View File

@ -1,94 +0,0 @@
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackConfig from '../webpack.config';
import path from 'path';
import Express from 'express';
import url from 'url';
import compression from 'compression';
import proxy from './proxy';
const server = new Express();
const port = 3000;
server.disable('x-powered-by');
server.use(compression());
server.use('/_health', (req, res) => {
res.sendStatus(200);
});
// In development: enable hot reloading
// In production: serve pre-built static assets
if (process.env.NODE_ENV !== 'production') {
const compiler = webpack(webpackConfig);
server.use(webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath,
}));
// Proxy requests to avoid CORS errors
// For /v2 endpoint, rewrite cookie Domains to localhost
// This makes the login & logout work in development
const proxyOptions = url.parse('https://store-stage.docker.com/v2');
proxyOptions.cookieRewrite = 'localhost';
server.use('/v2', proxy(proxyOptions));
server.use('/api', proxy('https://store-stage.docker.com/api'));
// Client-side hot reload
server.use(webpackHotMiddleware(compiler));
// Server-side hot reload
compiler.plugin('done', () => {
Object.keys(require.cache).forEach(id => {
if (id.indexOf(path.resolve(__dirname, '..', 'src')) !== -1 ||
id.indexOf(path.resolve(__dirname)) !== -1) {
delete require.cache[id];
}
});
});
} else {
server.use('/dist', Express.static('dist'));
}
server.use((req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<title>Docker Store</title>
<!-- Google Webmaster -->
<meta
name="google-site-verification"
content="u4812of_thlIvAZUrmDNK4dCM30Us49hReSqGAlttNM"
/>
<!-- Segment -->
<script type="text/javascript">
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.1.0";
analytics.load("PkiQ99OVaGVevM33khgOK18hXwwFSoPT");
}}();
</script>
<script src="//d2wy8f7a9ursnm.cloudfront.net/bugsnag-2.min.js"
data-apikey=${process.env.BUGSNAG_KEY}></script>
${process.env.NODE_ENV === 'production' ?
'<link rel="stylesheet" href="/dist/main.css">' : ''}
</head>
<body>
<div id="app"></div>
<script>
window.__INITIAL_STATE__ = ${'{}'}
</script>
<script src="/dist/main.js"></script>
</body>
</html>
`);
});
server.listen(port, error => {
if (error) {
console.error(error);
return;
}
console.info(`Server is listening at http://localhost:${port}.`);
});

View File

@ -1,209 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { createAction } from 'redux-actions';
import { jwt } from 'lib/utils/authHeaders';
import { post } from 'superagent';
import { readCookie } from 'lib/utils/cookie-handler';
const DOCKERCLOUD_HOST = '';
const ACCOUNT_BASE_URL = `${DOCKERCLOUD_HOST}/v2`;
// eslint-disable-next-line
export const ACCOUNT_FETCH_CURRENT_USER_INFORMATION = 'ACCOUNT_FETCH_CURRENT_USER_INFORMATION';
export const ACCOUNT_FETCH_USER_EMAILS = 'ACCOUNT_FETCH_USER_EMAILS';
export const ACCOUNT_FETCH_USER_INFORMATION = 'ACCOUNT_FETCH_USER_INFORMATION';
export const ACCOUNT_FETCH_USER_NAMESPACES = 'ACCOUNT_FETCH_USER_NAMESPACES';
export const ACCOUNT_FETCH_USER_ORGS = 'ACCOUNT_FETCH_USER_ORGS';
export const ACCOUNT_LOGOUT = 'ACCOUNT_LOGOUT';
export const ACCOUNT_TOGGLE_MAGIC_CARPET = 'ACCOUNT_TOGGLE_MAGIC_CARPET';
export const ACCOUNT_SELECT_NAMESPACE = 'ACCOUNT_SELECT_NAMESPACE';
export const login = (username, password) => {
return new Promise((resolve, reject) => {
const endpoint = '/v2/users/login/';
const req = post(endpoint)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.set('X-CSRFToken', readCookie('csrftoken'));
req.send({ username, password }).end((err, res) => {
if (err) {
const errors = {};
let body = {};
try {
body = JSON.parse(res.text);
} catch (e) {
errors._error = res.text;
reject(errors);
}
if (body.detail) {
errors._error = body.detail;
}
if (body.username) {
errors.username = body.username[0];
}
if (body.password) {
errors.password = body.password[0];
}
reject(errors);
return;
}
resolve();
});
});
};
export const signup = (email, username, password, redirect_value) => {
return new Promise((resolve, reject) => {
const endpoint = '/v2/users/signup/';
const req = post(endpoint)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.set('X-CSRFToken', readCookie('csrftoken'));
req.send({ email, username, password, redirect_value }).end((err, res) => {
if (err) {
const errors = {};
let body = {};
try {
body = JSON.parse(res.text);
} catch (e) {
errors._error = res.text;
reject(errors);
}
if (body.detail) {
errors._error = body.detail;
}
if (body.email) {
errors.email = body.email[0];
}
if (body.username) {
errors.username = body.username[0];
}
if (body.password) {
errors.password = body.password[0];
}
reject(errors);
return;
}
resolve();
});
});
};
export function accountLogout() {
const accountUrl = `${ACCOUNT_BASE_URL}/user/logout`;
return {
type: ACCOUNT_LOGOUT,
payload: {
promise:
request
.post(accountUrl)
.set(jwt())
.set('Accept', '*/*')
.end()
.then((res) => res.body),
},
};
}
/*
Sample JSON for user fetch
{
"id": "4663b07ca74111e492090242ac110143",
"username": "test1",
"full_name": "asdfasdfasdf",
"location": "asdf",
"company": "stuff",
"gravatar_email": "",
"is_staff": false,
"is_admin": false,
"profile_url": "",
"date_joined": "2014-09-23T19:42:13Z",
"gravatar_url": "https://secure.gravatar.com/avatar/88d62a9d7579193eea16d4f5ddee3f62.jpg?s=80&r=g&d=mm",
"type": "User"
}
*/
export function accountFetchUser({ namespace, isOrg }) {
const userOrOrg = isOrg ? 'orgs' : 'users';
const accountUrl = `${ACCOUNT_BASE_URL}/${userOrOrg}/${namespace}/`;
return {
type: ACCOUNT_FETCH_USER_INFORMATION,
payload: {
promise: request
.get(accountUrl)
.set(jwt())
.end()
.then((res) => res.body),
},
};
}
export function accountFetchCurrentUser({ shouldRedirectToLogin } = {}) {
const url = `${ACCOUNT_BASE_URL}/user/`;
return {
type: ACCOUNT_FETCH_CURRENT_USER_INFORMATION,
meta: {
shouldRedirectToLogin,
},
payload: {
promise:
request
.get(url)
.set(jwt())
.end()
.then((res) => res.body),
},
};
}
export function accountFetchUserEmails({ user }) {
const url = `${ACCOUNT_BASE_URL}/emailaddresses/`;
return {
type: ACCOUNT_FETCH_USER_EMAILS,
payload: {
promise:
request
.get(url)
.set(jwt())
.query({ user })
.end()
.then((res) => res.body),
},
};
}
export function accountFetchUserOrgs() {
// Fetches user objects for all namespaces you have access to - not just own
const url = `${ACCOUNT_BASE_URL}/user/orgs/`;
return {
type: ACCOUNT_FETCH_USER_ORGS,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(jwt())
.query({ page_size: 100 })
.end()
.then((res) => res.body),
},
};
}
export const accountSelectNamespace = createAction(ACCOUNT_SELECT_NAMESPACE);
export const accountToggleMagicCarpet =
createAction(ACCOUNT_TOGGLE_MAGIC_CARPET);

View File

@ -1,607 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { merge } from 'lodash';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import encodeForm from 'lib/utils/encodeForm';
import createBearer from 'lib/utils/create-bearer';
import { bearer } from 'lib/utils/authHeaders';
import bugsnagNotify from 'lib/utils/metrics';
import { isProductBundle } from 'lib/utils/product-utils';
import {
marketplaceFetchBundleDetail,
marketplaceFetchRepositoryDetail,
} from 'actions/marketplace';
/* eslint-disable max-len */
export const BILLING_CREATE_PAYMENT_METHOD = 'BILLING_CREATE_PAYMENT_METHOD';
export const BILLING_CREATE_PAYMENT_TOKEN = 'BILLING_CREATE_PAYMENT_TOKEN';
export const BILLING_CREATE_PRODUCT = 'BILLING_CREATE_PRODUCT';
export const BILLING_CREATE_PROFILE = 'BILLING_CREATE_PROFILE';
export const BILLING_CREATE_SUBSCRIPTION = 'BILLING_CREATE_SUBSCRIPTION';
export const BILLING_DELETE_PAYMENT_METHOD = 'BILLING_DELETE_PAYMENT_METHOD';
export const BILLING_DELETE_SUBSCRIPTION = 'BILLING_DELETE_SUBSCRIPTION';
export const BILLING_FETCH_INVOICE_PDF = 'BILLING_FETCH_INVOICE_PDF';
export const BILLING_FETCH_INVOICES = 'BILLING_FETCH_INVOICES';
export const BILLING_FETCH_LICENSE_DETAIL = 'BILLING_FETCH_LICENSE_DETAIL';
export const BILLING_FETCH_LICENSE_FILE = 'BILLING_FETCH_LICENSE_FILE';
export const BILLING_FETCH_PAYMENT_METHODS = 'BILLING_FETCH_PAYMENT_METHODS';
export const BILLING_FETCH_PRODUCT = 'BILLING_FETCH_PRODUCT';
export const BILLING_FETCH_PROFILE_SUBSCRIPTIONS = 'BILLING_FETCH_PROFILE_SUBSCRIPTIONS';
export const BILLING_FETCH_PROFILE_SUBSCRIPTIONS_AND_PRODUCTS = 'BILLING_FETCH_PROFILE_SUBSCRIPTIONS_AND_PRODUCTS';
export const BILLING_FETCH_PROFILE = 'BILLING_FETCH_PROFILE';
export const BILLING_SET_DEFAULT_PAYMENT_METHOD = 'BILLING_SET_DEFAULT_PAYMENT_METHOD';
export const BILLING_UPDATE_PROFILE = 'BILLING_UPDATE_PROFILE';
export const BILLING_UPDATE_SUBSCRIPTION = 'BILLING_UPDATE_SUBSCRIPTION';
const BILLING_HOST = '';
export const BILLING_BASE_URL = `${BILLING_HOST}/api/billing/v4`;
/*
* =============================================================================
* BILLING PRODUCT CALLS
* - Calls associated with the product catalog & plans
* =============================================================================
*/
export function billingFetchProduct({ id }) {
const url = `${BILLING_BASE_URL}/products/${id}/`;
return {
type: BILLING_FETCH_PRODUCT,
meta: { id },
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export const billingCreateProduct = ({ id, body }) => dispatch => {
const url = `${BILLING_BASE_URL}/products/${id}/`;
return dispatch({
type: BILLING_CREATE_PRODUCT,
meta: { id },
payload: {
promise:
request
.put(url)
.accept('application/json')
.set(bearer())
.send(body)
.end()
.then((res) => res.body),
},
});
};
/*
* =============================================================================
* USER BILLING CALLS
* - Calls associated with a User's billing/subscription information
* - Must include JWT and docker_id
* =============================================================================
*/
//------------------------------------------------------------------------------
// PAYMENTS & PAYMENT METHODS
//------------------------------------------------------------------------------
export function billingFetchPaymentMethods({ docker_id }) {
const url = `${BILLING_BASE_URL}/accounts/${docker_id}/payment-methods/`;
return {
type: BILLING_FETCH_PAYMENT_METHODS,
meta: {
docker_id,
shouldRedirectToLogin: true,
},
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
/*
NOTE: Stripe's api only accepts x-www-form-urlencoded data
NOTE: 'billforwardCreatePayment' is chained to this function and requires
`billforward-id`. The `billforward-id` is being passed through to the
create payment function via the META here.
*/
function createCardToken({
cvc,
exp_month,
exp_year,
name_first,
name_last,
number,
}, meta) {
// TODO ENV VAR - nathan - move const variables to environment
const stripeUrl = 'https://api.stripe.com/v1/tokens';
let stripeToken = 'pk_live_89IjovLdwh2MTzV7JsGJK3qk';
if (isStaging() || isDev()) {
stripeToken = 'pk_test_DMJYisAqHlWvFPgRfkKayAcF';
}
const card = {
name: `${name_first} ${name_last}`,
cvc,
number,
exp_month,
exp_year,
};
const encoded = encodeForm({ card });
return {
type: BILLING_CREATE_PAYMENT_TOKEN,
payload: {
promise: request.post(stripeUrl)
.accept('application/json')
.type('application/x-www-form-urlencoded')
.set('Authorization', createBearer(stripeToken))
.send(encoded)
.then((res) => {
return {
body: res.body,
meta,
};
}, (res) => {
/*
402 PAYMENT REQUIRED error occurs when user has incorrectly input
their card information. We don't care about user error - only if
something goes wrong on our end.
*/
if (res.status !== 402 || !isDev()) {
bugsnagNotify('STRIPE TOKEN ERR', res.message);
}
}),
},
};
}
/*
NOTE: This is the call to billforward that actually adds a payment method
to a user's billing profile.
This call requires a billforward-id (accountID) which is DIFFERENT than the
docker_id - Hence why 'billingCreatePaymentMethod' is NOT being wrapped by
the getAccountFromNamespace decorator.
*/
function billforwardCreatePayment({
'@type': type,
accountID,
cardID,
defaultPaymentMethod,
gateway,
stripeToken,
}) {
// TODO ENV VAR - nathan - move const variables to environment
let billforwardUrl =
'https://api.billforward.net/v1/tokenization/auth-capture';
let billforwardToken = '650cbe35-4aca-4820-a7d1-accec8a7083a';
if (isStaging() || isDev()) {
billforwardUrl =
'https://api-sandbox.billforward.net:443/v1/tokenization/auth-capture';
billforwardToken = 'ec687f76-c1b6-4d71-b919-4fe99202ca13';
}
return {
type: BILLING_CREATE_PAYMENT_METHOD,
payload: {
promise: request.post(billforwardUrl)
.accept('application/json')
.type('application/json')
.set('Authorization', createBearer(billforwardToken))
.send({
'@type': type,
accountID,
cardID,
defaultPaymentMethod,
gateway,
stripeToken,
})
.then((res) => res.body.results, (res) => {
if (!isDev()) {
bugsnagNotify('BF AUTH CAPTURE ERR', res.message);
}
}),
},
};
}
export const billingCreatePaymentMethod = ({
billforward_id,
cvc,
exp_month,
exp_year,
name_first,
name_last,
number,
}) => dispatch => {
const cardData = {
cvc,
exp_month,
exp_year,
name_first,
name_last,
number,
};
/*
NOTE:
Creating a payment method requires 2 parts
1 - Generating a token from Stripe's api
2 - Sending generated token to Billforward's api to attach payment method to
a relevant billing profile.
*/
return dispatch(createCardToken(cardData, { billforward_id }))
.then((res) => {
const tokenObject = res.value.body;
const stripeToken = tokenObject.id;
const cardID = tokenObject.card.id;
const accountID = res.value.meta.billforward_id;
const bfData = {
'@type': 'StripeAuthCaptureRequest',
accountID,
cardID,
defaultPaymentMethod: true,
gateway: 'Stripe',
stripeToken,
};
return dispatch(billforwardCreatePayment(bfData));
});
};
// Note: No response from this endpoint. Should refetch payment methods in then
export const billingSetDefaultPaymentMethod = ({ docker_id, card_id }) => {
const url =
`${BILLING_BASE_URL}/accounts/${docker_id}/payment-methods/${card_id}/`;
return {
type: BILLING_SET_DEFAULT_PAYMENT_METHOD,
payload: {
promise: request
.patch(url)
.set(bearer())
.send({
default: true,
})
.end()
.then((res) => res.body),
},
};
};
// Note: No response from this endpoint. Should refetch payment methods in then
export const billingDeletePaymentMethod = ({ docker_id, card_id }) => {
const url =
`${BILLING_BASE_URL}/accounts/${docker_id}/payment-methods/${card_id}/`;
return {
type: BILLING_DELETE_PAYMENT_METHOD,
payload: {
promise: request
.del(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
//------------------------------------------------------------------------------
// PROFILES
//------------------------------------------------------------------------------
export const billingFetchProfile = ({ docker_id, isOrg }) => dispatch => {
const url = `${BILLING_BASE_URL}/accounts/${docker_id}/`;
return dispatch({
type: BILLING_FETCH_PROFILE,
meta: { docker_id, isOrg },
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => {
/*
NOTE: required billforward-account-id must be pulled from the header
*/
return merge(
{},
res.body,
{ profile:
{ billforward_id: res.header['billforward-account-id'] },
}
);
}),
},
});
};
/* NOTE: A sample profile includes information such as...
* profile: {
* first_name,
* last_name,
* email,
* primary_phone,
* company_name,
* job_function,
* addresses,
* }
*/
export function billingCreateProfile({
docker_id,
profile,
}) {
const url = `${BILLING_BASE_URL}/accounts/${docker_id}/`;
return {
type: BILLING_CREATE_PROFILE,
meta: { docker_id },
payload: {
promise:
request
.put(url)
.set(bearer())
.send({ profile })
.end()
.then((res) => {
/*
NOTE: required billforward-account-id must be pulled from the header
*/
return merge(
{},
res.body,
{ profile:
{ billforward_id: res.header['billforward-account-id'] },
}
);
}),
},
};
}
export function billingUpdateProfile({
addresses,
company_name,
docker_id,
email,
first_name,
job_function,
last_name,
phone_primary,
}) {
const url = `${BILLING_BASE_URL}/accounts/${docker_id}/profile/`;
return {
type: BILLING_UPDATE_PROFILE,
meta: { docker_id },
payload: {
promise:
request
.patch(url)
.set(bearer())
.send({
addresses,
company_name,
email,
first_name,
last_name,
job_function,
phone_primary,
})
.end()
.then((res) => res.body),
},
};
}
//------------------------------------------------------------------------------
// INVOICES
//------------------------------------------------------------------------------
export function billingFetchInvoices({ docker_id }) {
const url = `${BILLING_BASE_URL}/invoices/`;
return {
type: BILLING_FETCH_INVOICES,
meta: { shouldRedirectToLogin: true },
payload: {
promise:
request
.get(url)
.query({ docker_id })
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export function billingFetchInvoicePDF({ docker_id, invoice_id }) {
const url = `${BILLING_BASE_URL}/invoices/${invoice_id}`;
// Undocumented responseType property
// https://github.com/visionmedia/superagent/pull/888/files
return {
type: BILLING_FETCH_INVOICE_PDF,
payload: {
promise:
request
.get(url)
.query({ docker_id })
.set(bearer())
.accept('application/pdf')
.responseType('blob')
.end()
.then(({ xhr }) => {
// xhr.response is a Blob
return xhr.response;
}),
},
};
}
//------------------------------------------------------------------------------
// LICENSES
//------------------------------------------------------------------------------
export const billingFetchLicenseDetail = ({ subscription_id }) => {
const url = `${BILLING_BASE_URL}/subscriptions/${subscription_id}/license-detail/`;
return {
type: BILLING_FETCH_LICENSE_DETAIL,
meta: { subscription_id },
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
export const billingFetchLicenseFile = ({ subscription_id }) => {
const url = `${BILLING_BASE_URL}/subscriptions/${subscription_id}/license-file/`;
return {
type: BILLING_FETCH_LICENSE_FILE,
meta: { subscription_id },
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
//------------------------------------------------------------------------------
// SUBSCRIPTIONS
//------------------------------------------------------------------------------
// TODO Kristie Replace this with the composition API when it is available
export const billingFetchProfileSubscriptionsAndProducts = ({ docker_id }) => dispatch => {
const url = `${BILLING_BASE_URL}/subscriptions/`;
return dispatch({
type: BILLING_FETCH_PROFILE_SUBSCRIPTIONS_AND_PRODUCTS,
payload: {
promise:
request
.get(url)
.query({ docker_id })
.set(bearer())
.end()
.then((res) => {
const results = res.body || [];
// For each subscription, we must fetch the product details
// from billing AND from product catalog
const promises = results.map((sub) => {
const { product_id: id, subscription_id } = sub;
const getProductDetails = [
// Do not make this call bubble up the err
dispatch(billingFetchProduct({ id })).catch(() => {}),
];
if (isProductBundle(id)) {
getProductDetails.push(
dispatch(marketplaceFetchBundleDetail({ id }))
);
// Fetch license details for DDC
getProductDetails.push(
dispatch(billingFetchLicenseDetail({ subscription_id }))
.catch(() => {}) // Do not make this call bubble up the err
);
} else {
getProductDetails.push(
dispatch(marketplaceFetchRepositoryDetail({ id }))
.catch(() => {})
);
}
return Promise.when(getProductDetails);
});
return Promise.when(promises).then(() => results);
}),
},
});
};
export const billingFetchProfileSubscriptions = ({ docker_id }) => dispatch => {
const url = `${BILLING_BASE_URL}/subscriptions/`;
return dispatch({
type: BILLING_FETCH_PROFILE_SUBSCRIPTIONS,
payload: {
promise:
request
.get(url)
.query({ docker_id })
.set(bearer())
.end()
.then((res) => res.body),
},
});
};
export function billingCreateSubscription({
docker_id,
eusa,
name,
pricing_components, // Array of objects
product_id,
product_rate_plan,
}) {
const params = {
docker_id,
eusa,
name,
pricing_components,
product_id,
product_rate_plan,
};
const url = `${BILLING_BASE_URL}/subscriptions/`;
return {
type: BILLING_CREATE_SUBSCRIPTION,
payload: {
promise:
request
.post(url)
.query({ docker_id })
.set(bearer())
.send(params)
.end()
.then((res) => res.body),
},
};
}
// body is the request body
export const billingUpdateSubscription =
({ subscription_id, body }) => dispatch => {
const url = `${BILLING_BASE_URL}/subscriptions/${subscription_id}/`;
return dispatch({
type: BILLING_UPDATE_SUBSCRIPTION,
meta: { subscription_id },
payload: {
promise:
request
.patch(url)
.accept('application/json')
.set(bearer())
.send(body)
.end()
.then((res) => res.body),
},
});
};
export const billingDeleteSubscription = ({ subscription_id }) => dispatch => {
const url = `${BILLING_BASE_URL}/subscriptions/${subscription_id}/`;
return dispatch({
type: BILLING_DELETE_SUBSCRIPTION,
meta: { subscription_id },
payload: {
promise:
request
.del(url)
.accept('application/json')
.set(bearer())
.send()
.end()
.then((res) => res.body),
},
});
};

View File

@ -1,15 +0,0 @@
export const INTERNAL_ROUTER_READY = 'INTERNAL_ROUTER_READY';
export const INTERNAL_STORE_IDLE = 'INTERNAL_STORE_IDLE';
export function internalRouterReady() {
return {
type: INTERNAL_ROUTER_READY,
};
}
export function internalStoreIdle(state) {
return {
type: INTERNAL_STORE_IDLE,
payload: state,
};
}

View File

@ -1,371 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { bearer } from 'lib/utils/authHeaders';
import { DEFAULT_SEARCH_PAGE_SIZE } from 'lib/constants/defaults';
import { OFFICIAL } from 'lib/constants/searchFilters/sources';
// TODO Kristie 5/17/16 Use actual API when it is ready
import searchPlatformFilters from 'lib/constants/searchFilters/platforms';
import isStaging from 'lib/utils/isStaging';
import sanitize from 'lib/utils/remove-undefined';
import trim from 'lodash/trim';
/* eslint-disable max-len */
export const MARKETPLACE_FETCH_AUTOCOMPLETE_SUGGESTIONS = 'MARKETPLACE_FETCH_AUTOCOMPLETE_SUGGESTIONS';
export const MARKETPLACE_FETCH_BUNDLE_DETAIL = 'MARKETPLACE_FETCH_BUNDLE_DETAIL';
export const MARKETPLACE_FETCH_BUNDLE_SUMMARY = 'MARKETPLACE_FETCH_BUNDLE_SUMMARY';
export const MARKETPLACE_FETCH_CATEGORIES = 'MARKETPLACE_FETCH_CATEGORIES';
export const MARKETPLACE_FETCH_MOST_POPULAR = 'MARKETPLACE_FETCH_MOST_POPULAR';
export const MARKETPLACE_FETCH_FEATURED = 'MARKETPLACE_FETCH_FEATURED';
export const MARKETPLACE_FETCH_PLATFORMS = 'MARKETPLACE_FETCH_PLATFORMS';
export const MARKETPLACE_FETCH_REPOSITORY_DETAIL = 'MARKETPLACE_FETCH_REPOSITORY_DETAIL';
export const MARKETPLACE_FETCH_REPOSITORY_SUMMARY = 'MARKETPLACE_FETCH_REPOSITORY_SUMMARY';
export const MARKETPLACE_SEARCH = 'MARKETPLACE_SEARCH';
export const MARKETPLACE_CREATE_REPOSITORY = 'MARKETPLACE_CREATE_REPOSITORY';
export const MARKETPLACE_EDIT_REPOSITORY = 'MARKETPLACE_EDIT_REPOSITORY';
export const MARKETPLACE_DELETE_REPOSITORY = 'MARKETPLACE_DELETE_REPOSITORY';
const MARKETPLACE_HOST = '';
const MARKETPLACE_BASE_URL = `${MARKETPLACE_HOST}/api/content/v1`;
const MARKETPLACE_PRIVATE_HOST = process.env.NODE_ENV !== 'production' || isStaging() ?
'https://mercury-content.s.stage-us-east-1.aws.dckr.io' :
'https://mercury-content.s.us-east-1.aws.dckr.io';
const MARKETPLACE_PRIVATE_BASE_URL = `${MARKETPLACE_PRIVATE_HOST}/api/private/content`;
/* eslint-enable max-len */
export function marketplaceSearch({
q,
category,
order,
page,
page_size = DEFAULT_SEARCH_PAGE_SIZE,
platform,
sort,
// Unless explicitly specified, do not include community results
source = OFFICIAL,
}) {
// Trim whitespace from query
// eslint-disable-next-line no-param-reassign
q = trim(q);
const params = {
q,
category,
order,
page,
page_size,
platform,
sort,
source,
};
// Track browse and search separately
if (q === '' && page_size === '99') {
analytics.track('browse', sanitize({ category, platform, source }));
} else {
analytics.track('search', sanitize({ q, category, platform, source }));
}
const url = `${MARKETPLACE_BASE_URL}/repositories/search`;
return {
type: MARKETPLACE_SEARCH,
meta: { q, page, page_size },
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.query(params)
.end()
.then((res) => res.body),
},
};
}
// Fetch the suggestions for the drop down on global search
export function marketplaceFetchAutocompleteSuggestions({ q }) {
const page = 1;
const page_size = 6;
const params = { q, page, page_size };
const url = `${MARKETPLACE_BASE_URL}/repositories/search`;
return {
type: MARKETPLACE_FETCH_AUTOCOMPLETE_SUGGESTIONS,
meta: params,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.query(params)
.end()
.then((res) => res.body),
},
};
}
export function marketplaceFetchRepositorySummary({ id }) {
const url =
`${MARKETPLACE_BASE_URL}/repositories/${id}/summary`;
return {
type: MARKETPLACE_FETCH_REPOSITORY_SUMMARY,
meta: { id },
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export function marketplaceFetchRepositoryDetail({ id }) {
const url = `${MARKETPLACE_BASE_URL}/repositories/${id}`;
return {
type: MARKETPLACE_FETCH_REPOSITORY_DETAIL,
meta: { id },
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export function marketplaceFetchBundleSummary({ id }) {
const url = `${MARKETPLACE_BASE_URL}/bundles/${id}/summary`;
return {
type: MARKETPLACE_FETCH_BUNDLE_SUMMARY,
meta: { id },
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export function marketplaceFetchBundleDetail({ id, shouldRedirectToLogin }) {
const url = `${MARKETPLACE_BASE_URL}/bundles/${id}`;
return {
type: MARKETPLACE_FETCH_BUNDLE_DETAIL,
meta: {
id,
shouldRedirectToLogin,
},
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
export function marketplaceFetchCategories() {
const url = `${MARKETPLACE_BASE_URL}/categories`;
return {
type: MARKETPLACE_FETCH_CATEGORIES,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
}
// TODO Kristie 5/17/16 Use actual API when it is ready
export function marketplaceFetchPlatforms() {
return {
type: MARKETPLACE_FETCH_PLATFORMS,
payload: {
promise: new Promise((resolve) => {
resolve(searchPlatformFilters);
}),
},
};
}
// -----------------------------------------------------------------------------
// Home page special searches
// -----------------------------------------------------------------------------
// Most Popular Images (fetch 9)
export function marketplaceFetchMostPopular() {
const params = { page_size: 9, source: OFFICIAL };
const url = `${MARKETPLACE_BASE_URL}/repositories/search`;
return {
type: MARKETPLACE_FETCH_MOST_POPULAR,
meta: params,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.query(params)
.end()
.then((res) => res.body),
},
};
}
// Newest Images (fetch 9)
export function marketplaceFetchFeatured() {
const params = {
page_size: 9,
category: 'featured',
source: OFFICIAL,
};
const url = `${MARKETPLACE_BASE_URL}/repositories/search`;
return {
type: MARKETPLACE_FETCH_FEATURED,
meta: params,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.query(params)
.end()
.then((res) => res.body),
},
};
}
export function marketplaceCreateRepository({
name,
namespace,
reponame,
publisher,
short_description,
full_description,
categories,
platforms,
source,
logo_url,
screenshots,
links,
eusa,
download_attribute,
instructions,
}) {
const url = `${MARKETPLACE_PRIVATE_BASE_URL}/repositories/`;
const body = {
name,
namespace,
reponame,
publisher,
short_description,
full_description,
categories,
platforms,
source,
logo_url,
screenshots,
links,
eusa,
download_attribute,
instructions,
};
return {
type: MARKETPLACE_CREATE_REPOSITORY,
meta: body,
payload: {
promise:
request
.post(url)
.set(bearer())
.send(body)
.accept('application/json')
.type('application/json')
.end()
.then((res) => res.body),
},
};
}
export function marketplaceEditRepository({
id,
name,
namespace,
reponame,
publisher,
short_description,
full_description,
categories,
platforms,
source,
logo_url,
screenshots,
links,
eusa,
download_attribute,
instructions,
}) {
const body = {
name,
namespace,
reponame,
publisher,
short_description,
full_description,
categories,
platforms,
logo_url,
source,
screenshots,
links,
eusa,
download_attribute,
instructions,
};
const url = `${MARKETPLACE_PRIVATE_BASE_URL}/repositories/${id}`;
return {
type: MARKETPLACE_EDIT_REPOSITORY,
meta: body,
payload: {
promise:
request
.patch(url)
.set(bearer())
.send(body)
.accept('application/json')
.type('application/json')
.end()
.then((res) => res.body),
},
};
}
export function marketplaceDeleteRepository({ id }) {
const url = `${MARKETPLACE_PRIVATE_BASE_URL}/repositories/${id}`;
return {
type: MARKETPLACE_DELETE_REPOSITORY,
payload: {
promise:
request
.del(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
}

View File

@ -1,88 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { normalize } from 'normalizr';
import { jwt } from 'lib/utils/authHeaders';
import { normalizers } from 'lib/constants/nautilus';
import { DEFAULT_TAGS_PAGE_SIZE } from 'lib/constants/defaults';
export const NAUTILUS_FETCH_TAGS_AND_SCANS = 'NAUTILUS_FETCH_TAGS_AND_SCANS';
export const NAUTILUS_FETCH_SCAN_DETAIL = 'NAUTILUS_FETCH_SCAN_DETAIL';
// Don't need to proxy the nautilus api
const NAUTILUS_HOST = '';
const NAUTILUS_BASE_URL = `${NAUTILUS_HOST}/api/nautilus/v1/repositories`;
const DOCKERHUB_HOST = '';
const DOCKERHUB_BASE_URL = `${DOCKERHUB_HOST}/v2/repositories`;
export const nautilusFetchTagsAndScans = ({
id,
namespace,
reponame,
page = 1,
page_size = DEFAULT_TAGS_PAGE_SIZE,
}) => {
const scansUrl = `${NAUTILUS_BASE_URL}/summaries/${namespace}/${reponame}/`;
const tagsUrl = `${DOCKERHUB_BASE_URL}/${namespace}/${reponame}/tags/`;
return {
type: NAUTILUS_FETCH_TAGS_AND_SCANS,
meta: { id, namespace, reponame, page, page_size },
payload: {
promise: new Promise((resolve, reject) => {
// Fetch all of the tags for this repository (paginated)
request.get(tagsUrl)
.set(jwt())
.query({ page, page_size })
.end()
.then(
// onSuccess of fetch tags, attempt to fetch the scans
({ body: tags }) => {
// Fetch all of the scans for this repository
request.get(scansUrl)
.set(jwt())
.end()
.then(
// onSuccess of fetch scans, return both tags and scans
({ body: scans }) => { resolve([tags, scans]); },
// onError of fetch scans, only return tags
() => { resolve([tags]); },
);
},
// onError of fetch tags, reject the promise (error)
reject
);
}),
},
};
};
export const nautilusFetchScanDetail = ({ id, namespace, reponame, tag }) => {
const url = `${NAUTILUS_BASE_URL}/result`;
const params = { detailed: 1, namespace, reponame, tag };
return {
type: NAUTILUS_FETCH_SCAN_DETAIL,
meta: { ...params, id },
payload: {
promise:
request
.get(url)
.set(jwt())
.query(params)
.end()
.then(({ body }) => {
// The API response contains a 'scan' resource within an object
// inside 'scan_details'
const { image, latest_scan_status, scan_details } = body;
// Normalize the API result using the 'scan' normalizer schema
// TODO Kristie 5/11/16 Do we need the reponame and tag?
const normalized = normalize({
latest_scan_status,
reponame: image.reponame,
tag: image.tag,
...scan_details,
}, normalizers.scan);
return normalized;
}),
},
};
};

View File

@ -1,316 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { bearer } from 'lib/utils/authHeaders';
/* eslint-disable max-len */
export const PUBLISH_ACCEPT_VENDOR_AGREEMENT = 'PUBLISH_ACCEPT_VENDOR_AGREEMENT';
export const PUBLISH_ADD_PRODUCT_TIERS = 'PUBLISH_ADD_PRODUCT_TIERS';
export const PUBLISH_CREATE_PRODUCT = 'PUBLISH_CREATE_PRODUCT';
export const PUBLISH_DELETE_PRODUCT_TIERS = 'PUBLISH_DELETE_PRODUCT_TIERS';
export const PUBLISH_FETCH_PRODUCT_DETAILS = 'PUBLISH_FETCH_PRODUCT_DETAILS';
export const PUBLISH_FETCH_PRODUCT_LIST = 'PUBLISH_FETCH_PRODUCT_LIST';
export const PUBLISH_FETCH_PRODUCT_TIERS = 'PUBLISH_FETCH_PRODUCT_TIERS';
export const PUBLISH_GET_PUBLISHERS = 'PUBLISH_GET_PUBLISHERS';
export const PUBLISH_GET_SIGNUP = 'PUBLISH_GET_SIGNUP';
export const PUBLISH_GET_VENDOR_AGREEMENT = 'PUBLISH_GET_VENDOR_AGREEMENT';
export const PUBLISH_SUBSCRIBE = 'PUBLISH_SUBSCRIBE';
export const PUBLISH_UPDATE_PRODUCT_DETAILS = 'PUBLISH_UPDATE_PRODUCT_DETAILS';
export const PUBLISH_UPDATE_PRODUCT_REPOS = 'PUBLISH_UPDATE_PRODUCT_REPOS';
export const PUBLISH_UPDATE_PRODUCT_TIERS = 'PUBLISH_UPDATE_PRODUCT_TIERS';
export const PUBLISH_UPDATE_PUBLISHER_INFO = 'PUBLISH_UPDATE_PUBLISHER_INFO';
const PUBLISH_HOST = '';
export const PUBLISH_BASE_URL = `${PUBLISH_HOST}/api/publish/v1`;
export const publishSubscribe = ({
first_name,
last_name,
company,
phone_number,
email,
}) => {
const url = `${PUBLISH_BASE_URL}/signups`;
return {
type: PUBLISH_SUBSCRIBE,
payload: {
promise:
request
.post(url)
.set(bearer())
.send({
first_name,
last_name,
company,
phone_number,
email,
})
.end()
.then(res => res.body),
},
};
};
export const publishGetSignup = () => {
const url = `${PUBLISH_BASE_URL}/signups`;
return {
type: PUBLISH_GET_SIGNUP,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
export const publishGetPublishers = () => {
const url = `${PUBLISH_BASE_URL}/publishers`;
return {
type: PUBLISH_GET_PUBLISHERS,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
// NOTE: no reducers for this action yet
export const publishUpdatePublisherInfo = (data) => {
const url = `${PUBLISH_BASE_URL}/publishers`;
/*
data:
{
"email": "nandhini@docker.com",
"first_name": "Nandhini",
"last_name": "Santhanam",
"company": "Docker,Inc",
"phone_number": "2222222222",
"links": [{
"name": "google.com",
"label": "website"
}]
}
*/
return {
type: PUBLISH_UPDATE_PUBLISHER_INFO,
payload: {
promise:
request
.patch(url)
.accept('application/json')
.set(bearer())
.send(data)
.end()
.then((res) => res.body),
},
};
};
export const publishFetchProductList = () => {
const url = `${PUBLISH_BASE_URL}/products`;
return {
type: PUBLISH_FETCH_PRODUCT_LIST,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
export const publishFetchProductDetails = ({ product_id }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}`;
return {
type: PUBLISH_FETCH_PRODUCT_DETAILS,
payload: {
promise:
request
.get(url)
.set(bearer())
.accept('application/json')
.end()
.then((res) => res.body),
},
};
};
export const publishGetVendorAgreement = () => {
const url = `${PUBLISH_BASE_URL}/vendor-agreement`;
return {
type: PUBLISH_GET_VENDOR_AGREEMENT,
payload: {
promise:
request
.get(url)
.accept('application/json')
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
// NOTE: no reducers for this action yet
export const publishCreateProduct = ({ name, status, repositories }) => {
const body = {
name,
status,
repositories,
};
const url = `${PUBLISH_BASE_URL}/products`;
return {
type: PUBLISH_CREATE_PRODUCT,
meta: body,
payload: {
promise:
request
.post(url)
.set(bearer())
.send(body)
.accept('application/json')
.type('application/json')
.end()
.then((res) => res.body),
},
};
};
// NOTE: no reducers for this action yet
export const publishUpdateProductRepos = ({ product_id, repoSources }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/repositories`;
return {
type: PUBLISH_UPDATE_PRODUCT_REPOS,
payload: {
promise:
request
.put(url)
.set(bearer())
.send(repoSources)
.accept('application/json')
.type('application/json')
.end()
.then((res) => res.body),
},
};
};
// NOTE: no reducers for this action yet
export const publishUpdateProductDetails = ({ product_id, details }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/`;
/*
details:
{
name, * required
status, * required
product_type,
full_description,
short_description,
categories,
platforms,
links,
screenshots,
}
*/
return {
type: PUBLISH_UPDATE_PRODUCT_DETAILS,
payload: {
promise:
request
.patch(url)
.set(bearer())
.send(details)
.accept('application/json')
.type('application/json')
.end()
.then((res) => res.body),
},
};
};
export const publishAcceptVendorAgreement = () => {
const url = `${PUBLISH_BASE_URL}/accept-vendor-agreement`;
return {
type: PUBLISH_ACCEPT_VENDOR_AGREEMENT,
payload: {
promise:
request
.post(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
export const publishFetchProductTiers = ({ product_id }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/rate-plans`;
return {
type: PUBLISH_FETCH_PRODUCT_TIERS,
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
export const publishCreateProductTiers = ({ product_id, tiersList }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/rate-plans`;
return {
type: PUBLISH_ADD_PRODUCT_TIERS,
payload: {
promise:
request
.post(url)
.set(bearer())
.send(tiersList)
.end()
.then((res) => res.body),
},
};
};
export const publishUpdateProductTiers = ({ product_id, tiersObject }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/rate-plans`;
return {
type: PUBLISH_UPDATE_PRODUCT_TIERS,
payload: {
promise:
request
.put(url)
.set(bearer())
.send(tiersObject)
.end()
.then((res) => res.body),
},
};
};
export const publishDeleteProductTiers = ({ product_id, tier_id }) => {
const url = `${PUBLISH_BASE_URL}/products/${product_id}/rate-plans/${tier_id}`;
return {
type: PUBLISH_DELETE_PRODUCT_TIERS,
payload: {
promise:
request
.del(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};

View File

@ -1,120 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import {
DEFAULT_TAGS_PAGE_SIZE,
DEFAULT_COMMENTS_PAGE_SIZE,
} from 'lib/constants/defaults';
import { jwt } from 'lib/utils/authHeaders';
/* eslint-disable max-len */
export const REPOSITORY_FETCH_COMMENTS = 'REPOSITORY_FETCH_COMMENTS';
export const REPOSITORY_FETCH_OWNED_NAMESPACES = 'REPOSITORY_FETCH_OWNED_NAMESPACES';
export const REPOSITORY_FETCH_REPOSITORIES_FOR_NAMESPACE = 'REPOSITORY_FETCH_REPOSITORIES_FOR_NAMESPACE';
export const REPOSITORY_FETCH_IMAGE_DETAIL = 'REPOSITORY_FETCH_IMAGE_DETAIL';
export const REPOSITORY_FETCH_IMAGE_TAGS = 'REPOSITORY_FETCH_IMAGE_TAGS';
/* eslint-enable max-len */
const DOCKERHUB_HOST = '';
const DOCKERHUB_BASE_URL = `${DOCKERHUB_HOST}/v2/repositories`;
export function repositoryFetchImageDetail({
namespace,
reponame,
}) {
const url = `${DOCKERHUB_BASE_URL}/${namespace}/${reponame}/`;
return ({
type: REPOSITORY_FETCH_IMAGE_DETAIL,
meta: { namespace, reponame },
payload: {
promise:
request
.get(url)
.accept('application/json')
.end()
.then((res) => res.body),
},
});
}
export function repositoryFetchImageTags({
// Temporary until the marketplace tags service is up
id,
isCertified = false,
namespace,
page = 1,
page_size = DEFAULT_TAGS_PAGE_SIZE,
reponame,
}) {
const url = `${DOCKERHUB_BASE_URL}/${namespace}/${reponame}/tags/`;
const params = { page_size, page };
return ({
type: REPOSITORY_FETCH_IMAGE_TAGS,
meta: { id, isCertified, namespace, reponame, page, page_size },
payload: {
promise:
request
.get(url)
.accept('application/json')
.query(params)
.end()
.then((res) => res.body),
},
});
}
export function repositoryFetchComments({
id,
isCertified = false,
namespace,
reponame,
page = 1,
page_size = DEFAULT_COMMENTS_PAGE_SIZE,
}) {
const url = `${DOCKERHUB_BASE_URL}/${namespace}/${reponame}/comments/`;
const params = { page_size, page };
return ({
type: REPOSITORY_FETCH_COMMENTS,
meta: { id, isCertified, page, page_size, namespace, reponame },
payload: {
promise:
request
.get(url)
.accept('application/json')
.query(params)
.end()
.then((res) => res.body),
},
});
}
export function repositoryFetchOwnedNamespaces() {
const url = `${DOCKERHUB_BASE_URL}/namespaces/`;
return {
type: REPOSITORY_FETCH_OWNED_NAMESPACES,
payload: {
promise:
request
.get(url)
.set(jwt())
.end()
.then((res) => res.body),
},
};
}
export function repositoryFetchRepositoriesForNamespace({ namespace }) {
const url = `${DOCKERHUB_BASE_URL}/${namespace}/`;
return {
type: REPOSITORY_FETCH_REPOSITORIES_FOR_NAMESPACE,
meta: { namespace },
payload: {
promise:
request
.get(url)
.set(jwt())
.query({ page_size: 0 })
.end()
.then((res) => res.body),
},
};
}

View File

@ -1,22 +0,0 @@
import { createAction } from 'redux-actions';
/* eslint-disable max-len */
// TODO: tests - nathan 06/3/16 Write tests for redirect
export const FINISH_PAGE_TRANSITION = 'FINISH_PAGE_TRANSITION';
export const REDIRECT = 'REDIRECT';
export const ROOT_CHANGE_GLOBAL_SEARCH_VALUE = 'ROOT_CHANGE_GLOBAL_SEARCH_VALUE';
export const START_PAGE_TRANSITION = 'START_PAGE_TRANSITION';
export const redirectTo = createAction(REDIRECT);
export const startPageTransition = createAction(START_PAGE_TRANSITION);
export const finishPageTransition = createAction(FINISH_PAGE_TRANSITION);
export const rootChangeGlobalSearchValue = ({ value }) => {
return {
type: ROOT_CHANGE_GLOBAL_SEARCH_VALUE,
payload: { value },
};
};
/* eslint-enable */

View File

@ -1,71 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import { bearer } from 'lib/utils/authHeaders';
/* eslint-disable max-len */
export const WHITELIST_FETCH_AUTHORIZATION = 'WHITELIST_FETCH_AUTHORIZATION';
export const WHITELIST_SUBSCRIBE_TO_BETA = 'WHITELIST_SUBSCRIBE_TO_BETA';
export const WHITELIST_AM_I_WAITING = 'WHITELIST_AM_I_WAITING';
const WHITELIST_HOST = '';
export const WHITELIST_BASE_URL = `${WHITELIST_HOST}/api/whitelist/v1`;
//------------------------------------------------------------------------------
// AUTHORIZATION
//------------------------------------------------------------------------------
export const whitelistFetchAuthorization = () => {
const url = `${WHITELIST_BASE_URL}/authorize`;
return {
type: WHITELIST_FETCH_AUTHORIZATION,
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then((res) => res.body),
},
};
};
// http://store-stage.docker.com/api/whitelist/v1/consumer_interest
export const whitelistAmIWaiting = () => {
const url = `${WHITELIST_BASE_URL}/consumer_interest`;
return {
type: WHITELIST_AM_I_WAITING,
payload: {
promise:
request
.get(url)
.set(bearer())
.end()
.then(res => res.body),
},
};
};
export const whitelistSubscribeToBeta = ({
first_name,
last_name,
company,
email,
}) => {
const url = `${WHITELIST_BASE_URL}/consumer_interest`;
return {
type: WHITELIST_SUBSCRIBE_TO_BETA,
payload: {
promise:
request
.post(url)
.set(bearer())
.send({
first_name,
last_name,
company,
email,
})
.end()
.then(res => res.body),
},
};
};

View File

@ -1,57 +0,0 @@
import React, { PropTypes } from 'react';
import {
Select,
Input,
} from 'common';
import css from './styles.css';
const Emails = ({
accountEmails,
fields,
onSelectChange,
initialized = {},
}) => {
let emails;
if (accountEmails.length <= 1) {
emails = (
<Input
readOnly
className={css.input}
value={accountEmails[0]}
id={'email'}
inputStyle={{ color: 'white', width: '100%' }}
underlineFocusStyle={{ borderColor: 'white' }}
placeholder="Email"
style={{ marginBottom: '14px', width: '' }}
/>
);
} else {
emails = (
<Select
{...fields.email}
onBlur={() => {}}
className={css.select}
placeholder="Email"
disabled={!!initialized.email}
style={{ marginBottom: '10px', width: '' }}
options={accountEmails.map((e) => {
return { label: e, value: e };
})}
onChange={onSelectChange}
ignoreCase
clearable={false}
/>
);
}
return emails;
};
Emails.propTypes = {
accountEmails: PropTypes.array.isRequired,
fields: PropTypes.any.isRequired,
initialized: PropTypes.object,
onSelectChange: PropTypes.func.isRequired,
};
export default Emails;

View File

@ -1,152 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import {
Button,
Input,
} from 'common';
import { whitelistSubscribeToBeta } from 'actions/whitelist';
import EmailSelect from './emailSelect';
import validate from './validations';
import css from './styles.css';
const { array, func, object } = PropTypes;
const dispatcher = {
whitelistSubscribeToBeta,
};
const fields = [
'firstName',
'lastName',
'company',
'email',
];
const mapStateToProps = ({ account }, props) => {
const initialValues = {
email: props && props.emails && props.emails[0].email,
};
return {
user: account && account.currentUser,
initialValues,
};
};
class BetaForm extends Component {
static propTypes = {
user: object,
emails: array.isRequired,
fields: object.isRequired,
onSuccess: func.isRequired,
handleSubmit: func.isRequired,
whitelistSubscribeToBeta: func.isRequired,
}
state = {
inProgress: false,
}
onSelectChange = (field) => (data) => {
const fieldObject = this.props.fields[field];
fieldObject.onChange(data.value);
}
onSubmit = (values) => {
this.setState({ inProgress: true });
// submit form data to api backend
const {
whitelistSubscribeToBeta: subscribeToBeta,
onSuccess,
} = this.props;
const {
firstName: first_name,
lastName: last_name,
company,
email,
} = values;
subscribeToBeta({
first_name,
last_name,
company,
email,
}).then(() => {
onSuccess(values);
this.setState({ inProgress: false });
});
}
render() {
const {
emails,
fields: propFields,
handleSubmit,
} = this.props;
const {
inProgress,
} = this.state;
const {
firstName,
lastName,
company,
} = propFields;
const submitText = inProgress ? 'Submitting...' : 'Request Beta Access';
const emailsArray = emails && emails.map(email => email.email);
return (
<form
key="beta-form"
className={css.main}
onSubmit={handleSubmit(this.onSubmit)}
>
<Input
{...firstName}
className={css.input}
id={'first'}
placeholder="First Name"
inputStyle={{ color: 'white', width: '100%' }}
underlineFocusStyle={{ borderColor: 'white' }}
errorText={firstName.touched && firstName.error}
/>
<Input
{...lastName}
className={css.input}
id={'last'}
placeholder="Last Name"
inputStyle={{ color: 'white', width: '100%' }}
underlineFocusStyle={{ borderColor: 'white' }}
errorText={lastName.touched && lastName.error}
/>
<Input
{...company}
className={css.input}
id={'company'}
placeholder="Company"
inputStyle={{ color: 'white', width: '100%' }}
underlineFocusStyle={{ borderColor: 'white' }}
errorText={company.touched && company.error}
/>
<EmailSelect
accountEmails={emailsArray}
fields={propFields}
onSelectChange={this.onSelectChange('email')}
/>
<Button
disabled={inProgress}
className={css.submit}
inverted type="submit"
>
{submitText}
</Button>
</form>
);
}
}
export default reduxForm({
form: 'betaForm',
fields,
validate,
},
mapStateToProps,
dispatcher,
)(BetaForm);

View File

@ -1,15 +0,0 @@
@import "utilities";
.main {
@mixin clearfix;
}
.input {
margin-bottom: $space-lg;
width: 100%;
}
.submit {
cursor: pointer;
margin: $space-xs 0;
}

View File

@ -1,18 +0,0 @@
export default (values) => {
const errors = {};
const {
firstName,
lastName,
company,
} = values;
if (!firstName) {
errors.firstName = 'Required';
}
if (!lastName) {
errors.lastName = 'Required';
}
if (!company) {
errors.company = 'Required';
}
return errors;
};

View File

@ -1,210 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import css from './styles.css';
import LoginForm from 'common/LoginForm';
import BetaForm from './BetaForm';
import {
accountFetchCurrentUser,
accountFetchUserEmails,
} from 'actions/account';
import {
whitelistAmIWaiting,
whitelistFetchAuthorization,
} from 'actions/whitelist';
import routes from 'lib/constants/routes';
import { readCookie } from 'lib/utils/cookie-handler';
import DDCBanner from 'components/Home/DDCBanner';
import HelpArticlesCards from 'components/Home/HelpArticlesCards';
const { array, bool, func, object, shape } = PropTypes;
const mapStateToProps = ({ account }) => {
const { currentUser, isCurrentUserBetalisted, userEmails } = account;
return {
currentUser: currentUser || {},
isCurrentUserBetalisted,
userEmails,
};
};
const dispatcher = {
accountFetchCurrentUser,
accountFetchUserEmails,
whitelistAmIWaiting,
whitelistFetchAuthorization,
};
@connect(mapStateToProps, dispatcher)
export default class Beta extends Component {
static propTypes = {
currentUser: object.isRequired,
isCurrentUserBetalisted: bool.isRequired,
location: object.isRequired,
userEmails: shape({
results: array,
}),
accountFetchCurrentUser: func.isRequired,
accountFetchUserEmails: func.isRequired,
whitelistAmIWaiting: func.isRequired,
whitelistFetchAuthorization: func.isRequired,
}
static contextTypes = {
router: shape({
push: func.isRequired,
}).isRequired,
}
state = { error: '' }
onError = (error) => {
this.setState({ error });
}
maybeRenderError() {
const { error } = this.state;
return error ? <div className={css.error}>{error}</div> : null;
}
betaSuccess = (values) => {
// onSuccess of beta signup submission
// We need to fetch whether a user has access to the whitelist
const { currentUser } = this.props;
this.props.whitelistAmIWaiting();
analytics.identify(currentUser.id, {
firstName: values.firstName,
lastName: values.lastName,
company: values.company,
}, () => {
analytics.track('Signed Up for Private Beta');
});
};
loginSuccess = () => {
analytics.track('Logged In on Beta Page');
// If user is on the whitelist then redirect them to the landing page
this.props.whitelistFetchAuthorization().then(() => {
this.context.router.replace({ pathname: routes.home() });
});
this.props.accountFetchCurrentUser().then(user => {
const username = user.username;
this.props.whitelistAmIWaiting();
this.props.accountFetchUserEmails({ user: username }).then(emails => {
// If user is already authorized, send them to the landing page
const primaryEmails = emails &&
emails.results &&
emails.results.filter(r => r.primary);
const primaryEmail = primaryEmails &&
primaryEmails.length &&
primaryEmails[0].email;
if (user && primaryEmail) {
analytics.identify(user.id, {
Docker_Hub_User_Name__c: user.username,
username: user.username,
dockerUUID: user.id,
point_of_entry: 'docker_store',
email: primaryEmail,
});
}
});
});
}
renderLogin() {
const endpoint = '/v2/users/login/';
let registerUrl = 'https://cloud.docker.com/';
let forgotPasswordUrl = 'https://cloud.docker.com/reset-password';
if (isStaging() || isDev()) {
registerUrl = 'https://cloud-stage.docker.com/';
forgotPasswordUrl = 'https://cloud-stage.docker.com/reset-password';
}
return (
<div className={css.login}>
<div className={css.subText}>
Login with your Docker ID to access the Beta
</div>
{this.maybeRenderError()}
<div className={css.form} key="login-sign-in">
<LoginForm
autoFocus
csrftoken={readCookie('csrftoken')}
endpoint={endpoint}
onSuccess={this.loginSuccess}
onError={this.onError}
/>
<div className={css.signupFlow}>
<a href={forgotPasswordUrl}>Forgot Password?</a> |
<a href={registerUrl}>Create an account</a>
</div>
</div>
</div>
);
}
renderBetaSignup() {
const { userEmails } = this.props;
return (
<div className={css.beta}>
<div className={css.subText}>
Currently Beta is invite-only.
Fill in the form below to request your invite.
</div>
<div className={css.form} key="beta-signup">
<BetaForm
onSuccess={this.betaSuccess}
emails={userEmails.results}
/>
</div>
</div>
);
}
renderHero() {
const {
currentUser,
isCurrentUserBetalisted,
userEmails,
} = this.props;
let form;
const isLoggedIn = currentUser && currentUser.id || false;
if (isLoggedIn && !!userEmails.results && userEmails.results.length > 0) {
if (isCurrentUserBetalisted) {
form = (
<div className={css.betaThanks}>
Thank you! We've received your request and we'll email you
when you are accepted to the Beta.
</div>
);
} else {
form = this.renderBetaSignup();
}
} else {
form = this.renderLogin();
}
return (
<div className={css.heroWrapper}>
<div className="wrapped">
<div className={css.heroContent}>
<div className={css.title}>Docker Store Beta</div>
{form}
</div>
</div>
</div>
);
}
render() {
return (
<div className={css.home}>
{this.renderHero()}
<DDCBanner isBetaPage />
<HelpArticlesCards />
</div>
);
}
}

View File

@ -1,132 +0,0 @@
@import "utilities";
$hero-height: 550px;
$featured-height: 250px;
$section-padding: 64px;
$color-ddc-background: #1b4255;
/* Hero and NavBar */
.home {
top: calc($topnav-height * -1);
position: relative;
width: 100%;
}
.heroWrapper {
min-height: $hero-height;
position: relative;
padding: $topnav-height 0;
/* set fallback in case multiple background images are not supported */
background-color: $color-dodger-blue;
background-image:
linear-gradient(to bottom, rgba(0, 191, 165, 0.8) 0%, transparent 100%),
linear-gradient(-74deg, transparent 90%, rgba(255, 255, 255, 0.23) 20%),
linear-gradient(-74deg, transparent 83%, rgba(255, 255, 255, 0.18) 15%),
linear-gradient(-74deg, transparent 76%, rgba(255, 255, 255, 0.1) 15%),
linear-gradient(to top, #127ab1, #1799e0, #1796db);
}
.title {
@mixin display2;
@mixin semiBold;
}
.logo {
margin: 0;
padding: 0;
}
.navBar {
display: flex;
height: $topnav-height;
color: $color-white;
align-items: center;
justify-content: space-between;
.rightNavLinks {
@mixin fontSize 2;
display: inline-flex;
& > * {
margin-right: $space-sm;
margin-left: $space-sm;
}
}
}
.heroContent {
@mixin flexCentered;
height: calc($hero-height - $topnav-height);
margin-top: $topnav-height;
flex-direction: column;
text-align: center;
color: $color-white;
}
.subText {
@mixin subhead;
margin-bottom: $space-xxxl;
}
.login {
display: flex;
flex-direction: column;
align-items: center;
}
.form {
display: inline-flex;
flex-direction: column;
width: $input-width-large;
button {
width: 100%;
}
.signupFlow {
display: flex;
justify-content: space-between;
margin: $space-xs 0;
}
}
.betaThanks {
width: $input-width-xlarge;
margin: $space-xl 0;
}
.error {
@mixin fontSize 2;
top: 0;
width: 100%;
text-align: center;
background: $color-variant-panic;
padding: $space-sm $space-xs;
}
/* Content sections */
.sectionWrapper {
padding-bottom: $section-padding;
& > div {
padding-top: $section-padding;
}
}
/* Help Articles */
.articles {
@mixin 2columnsResponsive 4, 8;
}
.helpArticlesCards {
align-self: stretch;
margin-bottom: $space-sm;
}
.helpArticlesHeadline {
@mixin fontSize 6;
margin-bottom: $space-sm;
}
.helpArticlesDescription {
@mixin fontSize 2;
margin-bottom: $space-xl;
}

View File

@ -1,167 +0,0 @@
import React, { Component, PropTypes } from 'react';
import css from './styles.css';
import {
ApplicationServicesIcon,
DatabasesIcon,
MessagingServicesIcon,
OperatingSystemsIcon,
ProgrammingLanguagesIcon,
StorageIcon,
AnalyticsIcon,
// ApplicationFrameworkIcon,
InfrastructureIcon,
BaseImagesIcon,
FeaturedImagesIcon,
ToolsIcon,
} from 'common';
import {
ANALYTICS_CATEGORY,
APPLICATION_FRAMEWORK_CATEGORY,
APPLICATION_INFRASTRUCTURE_CATEGORY,
APPLICATION_SERVICES_CATEGORY,
BASE_CATEGORY,
DATABASE_CATEGORY,
FEATURED_CATEGORY,
LANGUAGES_CATEGORY,
MESSAGING_CATEGORY,
OS_CATEGORY,
STORAGE_CATEGORY,
TOOLS_CATEGORY,
} from 'lib/constants/landingPage';
import ChevronArrows from '../ChevronArrows';
import findKey from 'lodash/findKey';
const { func, object } = PropTypes;
const noOp = () => {};
export default class CategoryCards extends Component {
static propTypes = {
categories: object.isRequired,
categoryDescriptions: object.isRequired,
goToCategory: func.isRequired,
}
state = {
firstCategoryIndex: 0,
}
setFirstCategoryIndex = (i) => () => {
this.setState({
firstCategoryIndex: i,
});
}
getIcon = (category) => {
switch (category.name) {
case ANALYTICS_CATEGORY:
return <AnalyticsIcon className={css.icon} />;
// TODO Get real icon for framework
case APPLICATION_FRAMEWORK_CATEGORY:
return <ApplicationServicesIcon className={css.icon} />;
case APPLICATION_INFRASTRUCTURE_CATEGORY:
return <InfrastructureIcon className={css.icon} />;
case APPLICATION_SERVICES_CATEGORY:
return <ApplicationServicesIcon className={css.icon} />;
case BASE_CATEGORY:
return <BaseImagesIcon className={css.icon} />;
case DATABASE_CATEGORY:
return <DatabasesIcon className={css.icon} />;
case FEATURED_CATEGORY:
return <FeaturedImagesIcon className={css.icon} />;
case LANGUAGES_CATEGORY:
return <ProgrammingLanguagesIcon className={css.icon} />;
case MESSAGING_CATEGORY:
return <MessagingServicesIcon className={css.icon} />;
case OS_CATEGORY:
return <OperatingSystemsIcon className={css.icon} />;
case STORAGE_CATEGORY:
return <StorageIcon className={css.icon} />;
case TOOLS_CATEGORY:
return <ToolsIcon className={css.icon} />;
default:
return <OperatingSystemsIcon className={css.icon} />;
}
}
renderCard = (category, index) => {
const { categories, goToCategory } = this.props;
const { name, description } = category;
const categoryName = findKey(categories, (label) => label === name);
const backgroundClass = `card${index}`;
return (
<div
key={index}
className={`${css.card} ${css[backgroundClass]}`}
onClick={goToCategory(categoryName)}
>
<div className={css.iconWrapper}>
{this.getIcon(category)}
</div>
<div className={css.cardContent}>
<div className={css.cardTitle}>{name}</div>
<div className={css.cardDescription}>{description}</div>
</div>
</div>
);
}
render() {
const {
analytics,
application_framework,
application_services,
application_infrastructure,
base,
database,
featured,
languages,
messaging,
os,
storage,
tools,
} = this.props.categoryDescriptions;
const categories = [
featured,
database,
base,
languages,
application_services,
messaging,
os,
analytics,
application_framework,
storage,
tools,
application_infrastructure,
];
const numCategories = categories.length;
const { firstCategoryIndex } = this.state;
const numShowing = 6;
const lastCategoryIndex = firstCategoryIndex + numShowing - 1;
const displayCategories =
categories.slice(firstCategoryIndex, lastCategoryIndex + 1);
const isPreviousDisabled = firstCategoryIndex === 0;
const isNextDisabled = lastCategoryIndex === numCategories - 1;
const onClickNext = isNextDisabled ?
noOp : this.setFirstCategoryIndex(lastCategoryIndex + 1);
const onClickPrevious = isPreviousDisabled ?
noOp : this.setFirstCategoryIndex(firstCategoryIndex - numShowing);
return (
<div>
<div className={css.sectionTitleWrapper}>
<div className={css.sectionTitle}>
Everything you need to build applications for your business
</div>
<ChevronArrows
isNextDisabled={isNextDisabled}
isPreviousDisabled={isPreviousDisabled}
onClickNext={onClickNext}
onClickPrevious={onClickPrevious}
/>
</div>
<div className={css.categoryCards}>
{displayCategories.map(this.renderCard)}
</div>
</div>
);
}
}

View File

@ -1,80 +0,0 @@
@import "utilities";
$category-icon-size: 100px;
.sectionTitle {
@mixin fontSize 7;
}
.sectionTitleWrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-xl;
}
.categoryCards {
@mixin 3columnsResponsive;
}
.card {
color: $color-white;
border-radius: $corner-size;
height: 240px;
margin-top: $space-sm;
position: relative;
overflow: hidden;
cursor: pointer;
}
.cardContent {
position: absolute;
top: $space-xxxl;
left: 90px;
right: 54px;
}
.icon {
@mixin square $category-icon-size;
fill: rgba(255, 255, 255, 0.35);
}
.iconWrapper {
position: absolute;
top: 30px;
left: calc($category-icon-size * -0.2);
}
.cardTitle {
@mixin fontSize 7;
font-weight: bold;
margin-bottom: $space-sm;
}
.cardDescription {
@mixin fontSize 2;
}
.card0 {
background-color: #8460ff;
}
.card1 {
background-color: #ff4081;
}
.card2 {
background-color: $color-fiord;
}
.card3 {
background-color: $color-dodger-blue;
}
.card4 {
background-color: $color-robins-egg-blue;
}
.card5 {
background-color: $color-koromiko;
}

View File

@ -1,49 +0,0 @@
import React, { Component, PropTypes } from 'react';
import css from './styles.css';
import { ChevronIcon } from 'common';
import classnames from 'classnames';
import { LARGE } from 'lib/constants/sizes';
const { bool, func } = PropTypes;
export default class ChevronArrows extends Component {
static propTypes = {
isNextDisabled: bool,
isPreviousDisabled: bool,
onClickNext: func.isRequired,
onClickPrevious: func.isRequired,
}
_onClickNext = () => {
if (!this.props.isNextDisabled) {
this.props.onClickNext();
}
}
_onClickPrevious = () => {
if (!this.props.isPreviousDisabled) {
this.props.onClickPrevious();
}
}
render() {
const { isNextDisabled, isPreviousDisabled } = this.props;
const nextClasses = classnames({
[css.icon]: true,
[css.disabled]: isNextDisabled,
});
const previousClasses = classnames({
[css.icon]: true,
[css.disabled]: isPreviousDisabled,
});
return (
<div>
<div className={css.previous} onClick={this._onClickPrevious}>
<ChevronIcon className={previousClasses} size={LARGE} />
</div>
<div className={css.next} onClick={this._onClickNext}>
<ChevronIcon className={nextClasses} size={LARGE} />
</div>
</div>
);
}
}

View File

@ -1,44 +0,0 @@
@import "utilities";
.icon {
fill: $color-fiord;
}
.disabled {
fill: $color-loblolly;
user-select: none;
}
.previous,
.next {
@mixin square $space-xxxl;
@mixin flexCentered;
color: $color-regent-gray;
display: inline-flex;
background-color: $color-white;
cursor: pointer;
&:hover {
background-color: $color-athens-gray;
}
}
.previous {
box-shadow:
inset 0 -1px 0 0 #e0e4e7,
inset 1px 0 0 0 rgba(224, 227, 231, 0.6);
}
.next {
box-shadow:
inset -1px 0 0 0 rgba(224, 227, 231, 0.6),
inset 0 -1px 0 0 #e0e4e7,
inset 1px 0 0 0 rgba(224, 227, 231, 0.6);
}
.previous > svg {
transform: rotate(90deg);
}
.next > svg {
transform: rotate(-90deg);
}

View File

@ -1,70 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import css from './styles.css';
import { AngledTitleBox } from 'common';
import { DDC } from 'lib/constants/landingPage';
import { DDC_ID, DDC_TRIAL_PLAN } from 'lib/constants/eusa';
import routes from 'lib/constants/routes';
const { bool } = PropTypes;
/* eslint-disable max-len */
export default class DDCBanner extends Component {
static propTypes = {
isBetaPage: bool,
}
render() {
const { isBetaPage } = this.props;
const { title, DTR, UCP, CS } = DDC;
const ddcDetail = routes.bundleDetail({ id: DDC_ID });
const ddcPurchase = routes.bundleDetailPurchase({ id: DDC_ID });
const ddcTrial = `${ddcPurchase}?plan=${DDC_TRIAL_PLAN}`;
let featuredContent;
if (isBetaPage) {
featuredContent = (
<AngledTitleBox
title="Featured Docker Solution"
className={css.titleBox}
/>
);
}
const ddcWrapperClass = isBetaPage ? css.ddcWrapperBeta : css.ddcWrapper;
const ddcSectionClass = isBetaPage ? css.ddcSectionBeta : css.ddcSection;
const buttonClass = isBetaPage ? css.buttonBeta : css.button;
return (
<div className={ddcWrapperClass}>
<div className="wrapped">
{featuredContent}
<div className={ddcSectionClass}>
<div>
<div className={css.titleAndButtons}>
<div className={css.DDCtitle}>{title}</div>
<Link to={ddcTrial} className={buttonClass}>
30 Day FREE Evaluation
</Link>
<Link to={ddcDetail} className={buttonClass}>
Get Datacenter
</Link>
</div>
</div>
<div className={css.DDCComponents}>
<div className={css.DDC}>
<div className={css.DDCName}>{DTR.name}</div>
<div className={css.DDCDescription}>{DTR.description}</div>
</div>
<div className={css.DDC}>
<div className={css.DDCName}>{UCP.name}</div>
<div className={css.DDCDescription}>{UCP.description}</div>
</div>
<div className={css.DDC}>
<div className={css.DDCName}>{CS.name}</div>
<div className={css.DDCDescription}>{CS.description}</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
/* eslint-enable */

View File

@ -1,89 +0,0 @@
@import "utilities";
$color-ddc-background: #1b4255;
/* DDC */
.ddcWrapper {
background-color: $color-ddc-background;
color: $color-white;
}
.ddcWrapperBeta {
background-color: $color-white;
color: $color-fiord;
}
.ddcSection,
.ddcSectionBeta {
@mixin 2columnsResponsive 4.5, 7.5;
}
.ddcSection {
padding-top: $space-xxxxl;
padding-bottom: $space-xxxxl;
}
.ddcSectionBeta {
padding-bottom: 64px;
}
.titleBox {
padding-top: 64px;
padding-bottom: $space-xl;
}
.titleAndButtons {
padding-bottom: $space-lg;
@media (min-width: $screen-md) {
padding-bottom: 0;
}
}
.DDCComponents {
@mixin 3columns 4, 4, 4;
}
.DDC {
flex-direction: column;
}
.DDCName {
@mixin fontSize 4;
margin-bottom: $space-xl;
}
.DDCDescription {
@mixin fontSize 1;
width: 95%;
}
.DDCtitle {
@mixin fontSize 7;
margin-bottom: $space-xl;
width: 80%;
& > a {
text-decoration: none;
}
}
.button,
.buttonBeta {
@mixin fontSize 2;
@mixin semiBold;
text-decoration: none;
padding: $space-sm $space-md;
display: inline-block;
box-sizing: border-box;
border-radius: $corner-size;
margin-right: $space-sm;
}
.button {
border: 2px solid $color-white;
}
.buttonBeta {
color: $color-dodger-blue;
border: 2px solid $color-dodger-blue;
}

View File

@ -1,99 +0,0 @@
import React, { Component, PropTypes } from 'react';
import css from './styles.css';
import { AngledTitleBox } from 'common';
import ChevronArrows from '../ChevronArrows';
import ImageSearchResult from 'marketplace/Search/ImageSearchResult';
const { array, bool, object, string } = PropTypes;
import classnames from 'classnames';
const noOp = () => {};
export default class FeaturedContentRow extends Component {
static propTypes = {
className: string,
description: string.isRequired,
headline: string.isRequired,
images: array.isRequired,
isFetching: bool,
location: object.isRequired,
title: string.isRequired,
}
state = {
firstImageIndex: 0,
}
setFirstImageIndex = (i) => () => {
this.setState({
firstImageIndex: i,
});
}
renderImage = (image) => {
const { location } = this.props;
return (
<ImageSearchResult
key={image.id}
className={css.imageWrapper}
image={image}
location={location}
/>
);
}
render() {
const {
description,
headline,
images = [],
title,
} = this.props;
const { firstImageIndex } = this.state;
const numImages = images.length;
let numShowing = 3;
if (typeof window !== 'undefined' &&
typeof window.matchMedia !== 'undefined') {
// Reduce the amount of results showing on a small screen
const isMedium = !window.matchMedia('(min-width: 960px)').matches;
const isSmall = !window.matchMedia('(min-width: 820px)').matches;
if (isSmall) {
numShowing = 1;
} else if (isMedium) {
numShowing = 2;
}
}
const lastImageIndex = firstImageIndex + numShowing - 1;
const displayImages = images.slice(firstImageIndex, lastImageIndex + 1);
// The first image showing is the first image available
const isPreviousDisabled = firstImageIndex === 0;
// The last image that could possibly be showing is the last image available
// or there is a not full row
const isNextDisabled = lastImageIndex >= numImages - 1;
const onClickNext = isNextDisabled ?
noOp : this.setFirstImageIndex(lastImageIndex + 1);
const onClickPrevious = isPreviousDisabled ?
noOp : this.setFirstImageIndex(firstImageIndex - numShowing);
const classes = classnames({
[css.contentRow]: true,
[this.props.className]: !!this.props.className,
});
return (
<div className={classes}>
<div className={css.contentDescription}>
<AngledTitleBox className={css.title} title={title} />
<div className={css.headline}>{headline}</div>
<div className={css.description}>{description}</div>
<ChevronArrows
isNextDisabled={isNextDisabled}
isPreviousDisabled={isPreviousDisabled}
onClickNext={onClickNext}
onClickPrevious={onClickPrevious}
/>
</div>
<div className={css.images}>
{displayImages.map(this.renderImage)}
</div>
</div>
);
}
}

View File

@ -1,43 +0,0 @@
@import "utilities";
.contentRow {
@mixin 2columns 3, 9;
}
.images {
lost-flex-container: row;
justify-content: center;
& > * {
lost-column: 1;
}
@media (min-width: $screen-md) {
& > * {
lost-column: 1/3;
}
}
}
.contentDescription {
padding-right: $space-lg;
min-width: 240px;
}
.title {
margin-bottom: $space-xl;
}
.headline {
@mixin fontSize 7;
margin-bottom: $space-sm;
}
.description {
@mixin fontSize 2;
margin-bottom: $space-xl;
}
div.imageWrapper {
padding-top: 0;
margin-bottom: $space-sm;
}

View File

@ -1,53 +0,0 @@
import React, { Component } from 'react';
import css from './styles.css';
import { Card, AngledTitleBox } from 'common';
import { helpArticles } from 'lib/constants/landingPage';
const {
// small,
// large,
blogPost,
} = helpArticles;
/* eslint-disable max-len */
export default class HelpArticlesCards extends Component {
// Two card article layout - leaving for when we switch back
// render() {
// return (
// <div className={css.articles}>
// <Card className={css.helpArticlesCards} shadow>
// <AngledTitleBox title="How To" className={css.titleBox} />
// <div className={css.helpArticlesHeadline}>{small.name}</div>
// <div className={css.helpArticlesDescription}>
// {small.description}
// </div>
// </Card>
// <Card className={css.helpArticlesCards} shadow>
// <AngledTitleBox title="Case Study" className={css.titleBox} />
// <div className={css.helpArticlesHeadline}>{large.name}</div>
// <div className={css.helpArticlesDescription}>
// {large.description}
// </div>
// </Card>
// </div>
// );
// }
render() {
const blogPostLink = 'https://blog.docker.com/2016/06/docker-store/';
return (
<div className={css.articles}>
<Card className={css.helpArticlesCards} shadow>
<AngledTitleBox title="Blog Post" className={css.titleBox} />
<div className={css.helpArticlesHeadline}>{blogPost.name}</div>
<div className={css.helpArticlesDescription}>
{blogPost.description}
</div>
<a href={blogPostLink} target="_blank" className={css.link}>
Read More
</a>
</Card>
</div>
);
}
}
/* eslint-enable */

View File

@ -1,32 +0,0 @@
@import "utilities";
/* Help Articles */
.articles {
@mixin wrapped;
padding-top: 64px;
padding-bottom: 64px;
}
.helpArticlesCards {
align-self: stretch;
}
.titleBox {
margin-bottom: $space-xl;
}
.helpArticlesHeadline {
@mixin fontSize 6;
margin-bottom: $space-sm;
}
.helpArticlesDescription {
@mixin fontSize 2;
margin-bottom: $space-xl;
}
.link {
@mixin fontSize 2;
@mixin semiBold;
color: $color-fiord;
}

View File

@ -1,147 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import css from './styles.css';
import { AutocompleteSearchBar } from 'common';
import {
marketplaceFetchAutocompleteSuggestions,
marketplaceSearch,
} from 'actions/marketplace';
import { rootChangeGlobalSearchValue } from 'actions/root';
import formatCategories from 'lib/utils/format-categories';
import routes from 'lib/constants/routes';
import debounce from 'lodash/debounce';
import { DEFAULT_DEBOUNCE_TIME } from 'lib/constants/defaults';
const { array, bool, func, object, shape } = PropTypes;
const mapStateToProps = ({ root }) => {
const { autocomplete } = root;
return { autocomplete };
};
const dispatcher = {
marketplaceFetchAutocompleteSuggestions,
marketplaceSearch,
rootChangeGlobalSearchValue,
};
/*
* Search to be used in the LandingPage with the same autocomplete functionality
* as the global search bar
*/
@connect(mapStateToProps, dispatcher)
export default class SearchWithAutocomplete extends Component {
static propTypes = {
autocomplete: shape({
isFetching: bool,
suggestions: array,
}),
location: object.isRequired,
marketplaceFetchAutocompleteSuggestions: func.isRequired,
marketplaceSearch: func.isRequired,
rootChangeGlobalSearchValue: func.isRequired,
}
static contextTypes = {
router: shape({
push: func.isRequired,
}).isRequired,
}
constructor(props) {
super(props);
// Make sure there is one debounce function per component instance
// http://stackoverflow.com/a/28046731/5965502
this.debouncedFetchAutocompleteSuggestions = debounce(
this.debouncedFetchAutocompleteSuggestions,
DEFAULT_DEBOUNCE_TIME
);
}
state = {
searchQuery: '',
}
componentWillUnmount() {
this.debouncedFetchAutocompleteSuggestions.cancel();
}
onSearchQueryChange = (e, value) => {
// Remove this synthetic event from the pool so that we can still access the
// event asyncronously (for debouncing)
// https://facebook.github.io/react/docs/events.html#event-pooling
e.persist();
// change the value showing in the search bar
this.setState({ searchQuery: value });
// fetch new suggestions (debounced)
this.debouncedFetchAutocompleteSuggestions(e, value);
}
onSelectAutosuggestItem = (value, item) => {
const { id } = item;
// Jump to the product detail page for this result
const detail = routes.imageDetail({ id });
this.context.router.push(detail);
}
debouncedFetchAutocompleteSuggestions = (e, value) => {
this.props.marketplaceFetchAutocompleteSuggestions({ q: value });
}
// Search Bar form has been submitted
search = (q) => {
// change the value in the global search bar
// this.props.rootChangeGlobalSearchValue({ value });
// fire search action and transition to search results page
this.props.marketplaceSearch({ q });
const pathname = routes.search();
const { state } = this.props.location;
// search from global search bar will have a query (q) (no page num or size)
const query = { q };
this.context.router.push({ pathname, query, state });
}
renderAutocompleteItem = (item, isHighlighted) => {
const { id, name, categories } = item;
const catNames = formatCategories(categories);
const catText = catNames ? ` in ${catNames}` : '';
const itemClass = isHighlighted ? css.highlightedResult : css.result;
return (
<div className={itemClass} key={id} id={id}>
<span className={css.resultName}>{name}</span>
<span className={css.resultCategories}>{catText}</span>
</div>
);
};
render() {
const { autocomplete } = this.props;
const { searchQuery } = this.state;
const { suggestions = [] } = autocomplete;
const menuTitle = (
<div className={css.menuTitle}>
Suggested Results
</div>
);
const getItemValue = (item) => item.id;
const classNames = {
icon: css.icon,
input: css.input,
wrapper: css.wrapper,
};
return (
<AutocompleteSearchBar
classNames={classNames}
getItemValue={getItemValue}
id="landingPage-search"
items={suggestions}
menuTitle={menuTitle}
onChange={this.onSearchQueryChange}
onSelect={this.onSelectAutosuggestItem}
onSubmit={this.search}
ref="autocomplete"
renderItem={this.renderAutocompleteItem}
value={searchQuery}
/>
);
}
}

View File

@ -1,59 +0,0 @@
@import "utilities";
/* Search Bar */
$search-bar-height: 58px;
/* SVG size SMALL */
$icon-offset: calc(($search-bar-height - $icon-size-small) / 2);
.wrapper {
width: 60%;
min-width: 400px;
}
.icon {
fill: $color-regent-gray;
position: absolute;
top: $icon-offset;
left: $icon-offset;
}
.input {
@mixin fontSize 4;
text-indent: calc($icon-offset + $space-sm);
height: $search-bar-height;
color: $color-regent-gray;
}
/* Search Autocomplete Results */
.result {
padding: $space-sm;
cursor: default;
text-align: left;
}
.highlightedResult {
padding: $space-sm;
cursor: default;
background: $color-variant-light-shade-hover;
text-align: left;
}
.menuTitle {
@mixin fontSize 2;
font-weight: 600;
padding: $space-md $space-sm;
cursor: default;
color: $color-fiord;
text-align: left;
}
.resultName {
@mixin fontSize 2;
color: $color-fiord;
}
.resultCategories {
@mixin fontSize 1;
color: $color-regent-gray;
}

View File

@ -1,140 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import css from './styles.css';
import { categoryDescriptions } from 'lib/constants/landingPage';
import CategoryCards from './CategoryCards';
import DDCBanner from './DDCBanner';
import FeaturedContentRow from './FeaturedContentRow';
import HelpArticlesCards from './HelpArticlesCards';
import SearchWithAutocomplete from './SearchWithAutocomplete';
import routes from 'lib/constants/routes';
const { array, bool, func, object, shape } = PropTypes;
const mapStateToProps = ({ marketplace, root }) => {
const { categories } = marketplace.filters;
const { mostPopular, featured } = root.landingPage;
return {
categories,
mostPopular,
featured,
};
};
@connect(mapStateToProps)
export default class Home extends Component {
static propTypes = {
autocomplete: shape({
isFetching: bool,
suggestions: array,
}),
categories: object.isRequired,
location: object.isRequired,
mostPopular: shape({
isFetching: bool,
images: array,
}),
featured: shape({
isFetching: bool,
images: array,
}),
}
static contextTypes = {
router: shape({
push: func.isRequired,
}).isRequired,
}
goToCategory = (category) => () => {
const query = { category };
const { state } = this.props.location;
const pathname = routes.search();
this.context.router.push({ pathname, query, state });
}
renderHero() {
return (
<div className={css.heroWrapper}>
<div className="wrapped">
<div className={css.heroContent}>
<div className={css.title}>Search The Docker Store</div>
<div className={css.subText}>
Find Trusted and Enterprise Ready Containers
</div>
<SearchWithAutocomplete location={this.props.location} />
</div>
</div>
</div>
);
}
renderFeaturedRow1() {
const { location, featured } = this.props;
// TODO Kristie 7/18/16 Make this sound better :)
const featuredHeadline = 'Featured Images';
const featuredTitle = 'Image Spotlight';
const featuredDescription =
'Curated images, ready to use. Verified and secure.';
return (
<div className={`wrapped ${css.sectionWrapper}`}>
<FeaturedContentRow
description={featuredDescription}
className={css.paddedRow}
headline={featuredHeadline}
images={featured.images}
isFetching={featured.isFetching}
location={location}
title={featuredTitle}
/>
</div>
);
}
renderFeaturedRow2() {
const { location, mostPopular } = this.props;
const popularHeadline = 'Most Popular Containers on the Store';
const popularTitle = 'Most Popular';
const popularDescription =
'Most popular content from our trusted partners on the Docker Store';
return (
<div className={`wrapped ${css.sectionWrapper}`}>
<FeaturedContentRow
description={popularDescription}
className={css.paddedRow}
headline={popularHeadline}
images={mostPopular.images}
isFetching={mostPopular.isFetching}
location={location}
title={popularTitle}
/>
</div>
);
}
renderCategoryCards() {
const { categories } = this.props;
return (
<div className="wrapped">
<CategoryCards
categories={categories}
categoryDescriptions={categoryDescriptions}
goToCategory={this.goToCategory}
/>
</div>
);
}
render() {
return (
<div className={css.home}>
{this.renderHero()}
{this.renderFeaturedRow1()}
<DDCBanner />
{this.renderFeaturedRow2()}
{this.renderCategoryCards()}
<HelpArticlesCards />
</div>
);
}
}

View File

@ -1,76 +0,0 @@
@import "utilities";
$hero-height: 500px;
$section-padding: 64px;
/* Hero and NavBar */
.home {
top: calc($topnav-height * -1);
position: relative;
width: 100%;
}
.heroWrapper {
min-height: $hero-height;
position: relative;
padding: $topnav-height 0;
/* set fallback in case multiple background images are not supported */
background-color: $color-dodger-blue;
background-image:
linear-gradient(to bottom, rgba(0, 191, 165, 0.8) 0%, transparent 100%),
linear-gradient(-74deg, transparent 90%, rgba(255, 255, 255, 0.23) 20%),
linear-gradient(-74deg, transparent 83%, rgba(255, 255, 255, 0.18) 15%),
linear-gradient(-74deg, transparent 76%, rgba(255, 255, 255, 0.1) 15%),
linear-gradient(to top, #127ab1, #1799e0, #1796db);
}
.title {
@mixin display2;
@mixin semiBold;
}
.logo {
margin: 0;
padding: 0;
}
.navBar {
display: flex;
height: $topnav-height;
color: $color-white;
align-items: center;
justify-content: space-between;
.rightNavLinks {
@mixin fontSize 2;
display: inline-flex;
& > * {
margin-right: $space-sm;
margin-left: $space-sm;
}
}
}
.heroContent {
@mixin flexCentered;
height: $hero-height;
flex-direction: column;
text-align: center;
color: $color-white;
}
.subText {
@mixin headline;
margin-bottom: $space-xxxl;
}
/* Content sections */
.sectionWrapper {
padding-bottom: $section-padding;
}
.paddedRow {
padding-top: $section-padding;
}

View File

@ -1,222 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { post } from 'superagent';
import Input from 'common/Input';
import Button from 'common/Button';
import css from './styles.css';
import isValidPassword from 'lib/utils/password-validator';
const { string, func, bool } = PropTypes;
// Not sure where this should go yet
const isValidDockerID = (val = '') => val.length;
// NOTE: This component has to be refactored, as it was developed
// in a rush for Docker Store's launch
export default class LoginForm extends Component {
static propTypes = {
endpoint: string,
onSuccess: func,
onError: func,
autoFocus: bool,
csrftoken: string,
}
static defaultProps = {
onSuccess() {},
onError() {},
}
static getInput(value, label) {
return (
<Input
className={css.input}
value={value}
id={label}
inputStyle={{ color: 'white' }}
underlineFocusStyle={{ borderColor: 'white' }}
hintText={label}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
/>
);
}
state = {
id: '',
password: '',
inProgress: false,
errors: {
id: '',
password: '',
// This field is disregarded in the form.
// If we encounter it through the ajax call,
// it's passed as-is to the onError callback()
// detail: '',
},
}
getHintStyle(value) {
return !value ? { color: 'white', opacity: 0.6 } : null;
}
handleChange(which, ev) {
this.setState({ [which]: ev.target.value });
}
isValidState() {
const { id, password } = this.state;
return isValidDockerID(id) && isValidPassword(password);
}
validateDefault(which, { allowEmpty = false } = {}) {
const { id, password } = this.state;
let errors = Object.assign({}, this.state.errors);
const errorId = () => {
if ((allowEmpty && !id.length) || isValidDockerID(id)) return null;
return ' ';
};
const errorPassword = () => {
if ((allowEmpty && !password.length) || isValidPassword(password)) {
return null;
}
return ' ';
};
switch (which) {
case 'id': errors.id = errorId(); break;
case 'password': errors.password = errorPassword(); break;
default:
errors = {
id: errorId(),
password: errorPassword(),
};
}
this.setState({ errors });
}
handleSubmit = (ev) => {
ev.preventDefault();
if (!this.isValidState()) {
this.validateDefault();
return;
}
const { password } = this.state;
const id = this.state.id && this.state.id.toLowerCase();
const { endpoint, onSuccess, onError, csrftoken } = this.props;
this.setState({
inProgress: true,
errors: {
id: null,
password: null,
},
});
const req = post(endpoint)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
if (csrftoken) {
req.set('X-CSRFToken', csrftoken);
}
req.send({ password, username: id }).end((err, res) => {
this.setState({ inProgress: false });
if (err) {
const errors = {};
let body = {};
try {
body = JSON.parse(res.text);
} catch (e) {
onError(res.text);
return;
}
if (err.status === 401) {
errors.id = ' ';
errors.password = ' ';
}
if (body.detail) {
onError(body.detail);
}
if (body.username) {
errors.id = body.username[0];
}
if (body.password) {
errors.password = body.password[0];
}
if (Object.keys(errors).length) {
this.setState({ errors });
}
return;
}
onSuccess({ username: id, token: res.body.token });
});
}
resetFields() {
this.setState({ id: '', password: '' });
}
renderInputID(value) {
const { autoFocus } = this.props;
const baseInput = LoginForm.getInput(value, 'Docker ID');
const errorText = this.state.errors.id;
const hintStyle = this.getHintStyle(value);
const onChange = (ev) => this.handleChange('id', ev);
const onBlur = () => this.validateDefault('id', { allowEmpty: true });
return React.cloneElement(
baseInput,
{ onChange, onBlur, hintStyle, errorText, autoFocus }
);
}
renderInputPassword(value) {
const baseInput = LoginForm.getInput(value, 'Password');
const errorText = this.state.errors.password;
const hintStyle = this.getHintStyle(value);
const onChange = (ev) => this.handleChange('password', ev);
const onBlur = () => this.validateDefault('password', { allowEmpty: true });
return React.cloneElement(
baseInput,
{ onChange, onBlur, hintStyle, errorText, type: 'password' }
);
}
render() {
const { id, password, inProgress } = this.state;
const inputID = this.renderInputID(id);
const inputPassword = this.renderInputPassword(password);
return (
<div className={css.main}>
<form onSubmit={this.handleSubmit} key="login-form">
{inputID}
{inputPassword}
<Button
disabled={inProgress}
className={css.login}
inverted
type="submit"
>
Login
</Button>
</form>
</div>
);
}
}

View File

@ -1,19 +0,0 @@
@import "utilities";
.main {
@mixin clearfix;
}
.input {
margin-bottom: $space-lg;
width: 100%;
}
.login {
cursor: pointer;
float: right;
margin-left: 0;
margin-right: 0;
margin-top: $space-xl;
width: 110px;
}

View File

@ -1,100 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import staticBox from 'lib/decorators/StaticBox';
import { readCookie } from 'lib/utils/cookie-handler';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import {
accountFetchCurrentUser,
accountFetchUserEmails,
} from 'actions/account';
import { DockerFlatIcon as DockerFlat } from 'common/Icon';
import LoginForm from './LoginForm';
import css from './styles.css';
import qs from 'qs';
import {
isValidUrl,
isQsPathSecure,
} from 'lib/utils/url-utils';
import routes from 'lib/constants/routes';
const { func, shape } = PropTypes;
const dispatcher = { accountFetchCurrentUser, accountFetchUserEmails };
@staticBox
@connect(null, dispatcher)
export default class Login extends Component {
static propTypes = {
accountFetchCurrentUser: func.isRequired,
accountFetchUserEmails: func.isRequired,
}
static contextTypes = {
router: shape({
replace: func.isRequired,
}).isRequired,
}
state = { error: '' }
onError = (error) => {
this.setState({ error });
}
navigate = () => {
let query = window && window.location.search;
// Scrape off first '?' from search
if (query[0] === '?') {
query = query.substring(1, query.length);
}
const { next } = qs.parse(query);
const nextUrl = `${window.location.origin}${next}`;
if (isValidUrl(nextUrl) && isQsPathSecure(next)) {
window.location = nextUrl;
} else {
window.location = routes.home();
}
}
maybeRenderError() {
const { error } = this.state;
return error ? <div className={css.error}>{error}</div> : null;
}
render() {
const endpoint = '/v2/users/login/';
let registerUrl = 'https://cloud.docker.com/';
let forgotPasswordUrl = 'https://cloud.docker.com/reset-password';
if (isStaging() || isDev()) {
registerUrl = 'https://cloud-stage.docker.com/';
forgotPasswordUrl = 'https://cloud-stage.docker.com/reset-password';
}
return (
<div>
{this.maybeRenderError()}
<div className={css.banner}>
<Link to="/">
<DockerFlat />
</Link>
<h1>Welcome to the Docker Store</h1>
<p>Login with your <strong>Docker ID</strong></p>
</div>
<div className={css.form}>
<LoginForm
autoFocus
csrftoken={readCookie('csrftoken')}
endpoint={endpoint}
onSuccess={this.navigate}
onError={this.onError}
/>
</div>
<div className={css.more}>
<a href={forgotPasswordUrl}>Forgot Password?</a>
<a href={registerUrl}>Create Account</a>
</div>
</div>
);
}
}

View File

@ -1,60 +0,0 @@
@import "utilities";
.banner {
text-align: center;
margin-bottom: $space-xxxxl;
svg {
@mixin square 70px;
fill: $color-white;
}
h1 {
@mixin fontSize 5;
font-weight: 700;
margin: 0;
}
p {
@mixin fontSize 2;
}
}
.form {
margin-bottom: $space-xxxxl;
}
.more {
position: absolute;
bottom: 20px;
width: 100%;
a {
@mixin linkUnstyled;
display: inline-block;
width: 50%;
text-align: left;
padding-left: $space-lg;
&:first-child {
padding-left: 0;
text-align: right;
padding-right: $space-lg;
border-right: 1px solid white;
}
&:hover {
text-decoration: underline;
}
}
}
.error {
@mixin fontSize 2;
position: absolute;
top: 0;
width: 100%;
text-align: center;
background: $color-variant-panic;
padding: $space-sm $space-xs;
}

View File

@ -1,25 +0,0 @@
import React, { Component } from 'react';
import css from './styles.css';
// Note: must include image like this so that webpack picks it up
import image404 from 'lib/images/404@2x.png';
/* eslint-disable max-len */
export default class RouteNotFound404 extends Component {
render() {
return (
<div className={css.wrapper}>
<div>
<img
className={css.img}
alt="404 Route Not Found"
src={image404}
/>
</div>
</div>
);
}
}
/* eslint-enable */

View File

@ -1,17 +0,0 @@
@import "utilities";
.wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
& > div {
flex: 1 auto;
align-self: center;
}
}
.img {
/* scale to persist square */
max-width: 100vw;
max-height: 100vw;
}

View File

@ -1,37 +0,0 @@
import React, { PropTypes } from 'react';
import { Select } from '../lib/common';
import { noOp } from '../lib/helpers';
import css from './styles.css';
const { string, func, array } = PropTypes;
const Accounts = ({ options, onSelectChange, selectedNamespace }) => {
let accountSelects = null;
if (options.length > 1) {
accountSelects = (
<div className={css.account}>
<div className={css.sectionTitle}>
Account
</div>
<Select
value={selectedNamespace}
className={css.accountSelect}
clearable={false}
ignoreCase
onBlur={noOp}
onChange={onSelectChange}
options={options}
placeholder="Account"
/>
</div>
);
}
return accountSelects;
};
Accounts.propTypes = {
selectedNamespace: string.isRequired,
onSelectChange: func.isRequired,
options: array.isRequired,
};
export default Accounts;

View File

@ -1,21 +0,0 @@
@import "utilities";
.account {
@mixin fontSize 2;
color: $color-regent-gray;
margin: $space-sm 0;
display: inline-flex;
justify-content: flex-end;
align-items: center;
}
.sectionTitle {
margin: $space-sm 0;
}
.accountSelect {
width: 150px;
height: $select-height;
width: $select-width;
margin-left: $space-sm;
}

View File

@ -1,161 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import map from 'lodash/map';
// Blob is a polyfill to support older browsers
require('lib/utils/blob');
import { saveAs } from 'lib/utils/file-saver';
import classnames from 'classnames';
import {
Card,
DownloadIcon,
FetchingError,
FullscreenLoading,
} from '../lib/common';
import { billingFetchInvoicePDF } from 'actions/billing';
import css from './styles.css';
const PENDING = 'Pending';
const { array, bool, func, object, shape, string } = PropTypes;
const mapStateToProps = ({ billing }) => {
const { invoices } = billing;
return {
invoices,
};
};
const dispatcher = {
fetchInvoicePDF: billingFetchInvoicePDF,
};
@connect(mapStateToProps, dispatcher)
export default class InvoicesView extends Component {
static propTypes = {
invoices: shape({
error: string,
isFetching: bool.isRequired,
results: array.isRequired,
}).isRequired,
fetchInvoicePDF: func.isRequired,
selectedUser: object.isRequired,
}
// When an invoice is being downloaded, there will be a key for invoice id
// with status information: [invoiceId]: { isDownloading: bool, error: bool }
state = {}
downloadInvoice = ({ invoice_id, issuedDate }) => () => {
const { fetchInvoicePDF, selectedUser } = this.props;
const { id: docker_id } = selectedUser;
// Make sure to not override any existing invoice download statuses
this.setState({
[invoice_id]: {
isDownloading: true,
error: false,
},
}, () => {
fetchInvoicePDF({ docker_id, invoice_id })
.then((res) => {
// value contains the API response
const downloadContent = res.value;
this.setState({
[invoice_id]: {
isDownloading: false,
error: false,
},
});
const blob = new Blob([downloadContent], { type: 'application/pdf' });
saveAs(blob, `Docker Invoice ${issuedDate}.pdf`);
})
.catch(() => {
this.setState({
[invoice_id]: {
isDownloading: false,
error: true,
},
});
});
});
};
renderInvoiceRow = (invoice) => {
const {
bf_invoice_id: invoice_id,
cost,
currency,
issued,
payment_received,
state,
subscriptions,
} = invoice;
// Do not render invoices in the 'Pending' state
if (state === PENDING) {
return null;
}
const { isDownloading, error } = this.state[invoice_id] || {};
const issuedDate = new Date(issued).toDateString();
const paid = new Date(payment_received).toDateString();
const costAndCurrency = `$${cost} ${currency}`;
const subscriptionsList = map(subscriptions, (sub) => {
return (<div key={sub.subscription_id}>{sub.subscription_name}</div>);
});
let downloadText = 'PDF';
if (isDownloading && !error) {
downloadText = 'Loading...';
} else if (isDownloading && error) {
downloadText = 'Error';
}
const classes = classnames({
[css.downloadLicense]: true,
[css.downloadLicenseError]: isDownloading && error,
});
return (
<div key={invoice_id} className={css.row}>
<div>{issuedDate}</div>
<div>{subscriptionsList}</div>
<div>{costAndCurrency}</div>
<div>{paid}</div>
<div
className={classes}
onClick={this.downloadInvoice({ invoice_id, issuedDate })}
>
<DownloadIcon />
{downloadText}
</div>
</div>
);
}
render() {
const { isFetching, error, results } = this.props.invoices;
if (isFetching) {
return <FullscreenLoading />;
} else if (error) {
return (
<div className={css.fetchingError}>
<FetchingError resource="your invoices" />
</div>
);
}
let content;
if (results.length < 1) {
content = <div className={css.noInvoices}>No invoices</div>;
} else {
content = map(results, this.renderInvoiceRow);
}
return (
<Card title="Invoices" shadow>
<div className={css.table}>
<div className={css.head}>
<div>Date</div>
<div>Plans & Subscriptions</div>
<div>Amount</div>
<div>Paid</div>
<div></div>
</div>
{content}
</div>
</Card>
);
}
}

View File

@ -1,47 +0,0 @@
@import "utilities";
.table {
.head,
.row {
@mixin 4columns 2, 4.5, 2, 2, 1.5;
}
.row {
align-items: center;
}
.row + .row {
margin-top: $space-xxl;
}
.head {
font-weight: bold;
color: $color-variant-dull;
margin: $space-sm 0;
}
}
.noInvoices {
text-align: center;
}
.downloadLicense {
@mixin fontSize 2;
display: flex;
font-weight: bold;
color: $color-dodger-blue;
align-items: center;
justify-content: flex-end;
cursor: pointer;
& > svg {
fill: $color-dodger-blue;
margin-right: $space-xxs;
}
}
.downloadLicenseError {
color: $color-carnation;
& > svg {
fill: $color-carnation;
}
}

View File

@ -1,215 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import {
countryOptions,
} from '../../lib/countries';
import {
Button,
Input,
Select,
} from '../../lib/common';
import EmailSelect from '../EmailSelect';
import sortBy from 'lodash/sortBy';
import merge from 'lodash/merge';
import css from './styles.css';
const { array, func, object } = PropTypes;
class ContactForm extends Component {
static propTypes = {
emailsArray: array.isRequired,
fields: object.isRequired,
handleSubmit: func.isRequired,
}
onSelectChange = (field) => (data) => {
const fieldObject = this.props.fields[field];
fieldObject.onChange(data.value);
}
render() {
const {
emailsArray,
fields: propFields,
} = this.props;
const {
address1,
city,
company,
country,
firstName,
job,
lastName,
phone,
postalCode,
province,
} = propFields;
return (
<div>
<div className={css.title} >
Contact Information
</div>
<div className={css.form}>
<div className={css.row}>
<Input
{...firstName}
id={'first'}
placeholder="First Name"
errorText={firstName.touched && firstName.error}
/>
<Input
{...lastName}
id={'last'}
placeholder="Last Name"
errorText={lastName.touched && lastName.error}
/>
</div>
<div className={css.row}>
<Input
{...company}
id={'company'}
placeholder="Company"
/>
<EmailSelect
accountEmails={emailsArray}
fields={propFields}
onSelectChange={this.onSelectChange('email')}
/>
</div>
<div className={css.row}>
<Input
{...job}
id={'job'}
placeholder="Job"
/>
<Input
{...phone}
id={'phone'}
placeholder="Phone"
/>
</div>
<div className={css.title} >
Address
</div>
<div className={css.fullRow}>
<Input
{...address1}
style={{ width: '100%' }}
id={'address'}
placeholder="Address"
/>
</div>
<div className={css.row}>
<Input
{...city}
id={'city'}
placeholder="City"
/>
<Input
{...postalCode}
id={'postalCode'}
placeholder="Postal Code"
/>
</div>
<div className={css.row}>
<Input
{...province}
id={'province'}
placeholder="Province"
/>
<Select
{...country}
onBlur={() => {}}
className={css.select}
placeholder="Country"
style={{ marginBottom: '10px', width: '' }}
options={countryOptions}
onChange={this.onSelectChange('country')}
ignoreCase
clearable={false}
errorText={
country.touched && country.error ? country.error : ''
}
/>
</div>
<Button
className={css.submit}
type="submit"
onClick={this.props.handleSubmit}
>
Update Contact Information
</Button>
</div>
</div>
);
}
}
const fields = [
'address1',
'city',
'company',
'country',
'email',
'firstName',
'job',
'lastName',
'phone',
'postalCode',
'province',
];
const mapStateToProps = ({ account, billing }) => {
const {
profiles,
} = billing;
const {
namespaceObjects,
selectedNamespace,
userEmails,
} = account;
const emailList = sortBy(userEmails.results, o => !o.primary);
const emailsArray = emailList.map((emailObject) => {
return emailObject.email;
});
const selectedUser = namespaceObjects.results[selectedNamespace] || {};
const billingProfile = profiles.results[selectedUser.id];
const initialData = {
account: selectedNamespace,
email: emailList[0],
};
let initialized = {};
if (!!billingProfile) {
const selectedEmail = billingProfile.email;
if (emailsArray.indexOf(selectedEmail) < 0) {
emailsArray.unshift(selectedEmail);
}
const address =
billingProfile.addresses && billingProfile.addresses[0] || {};
initialized = {
address1: address.address_line_1,
city: address.city,
company: billingProfile.company_name,
country: address.country,
email: billingProfile.email,
firstName: billingProfile.first_name,
job: billingProfile.job_function,
lastName: billingProfile.last_name,
phone: billingProfile.phone_primary,
postalCode: address.post_code,
province: address.province,
};
merge(initialData, initialized);
}
return {
emailsArray,
initialValues: initialData,
};
};
export default reduxForm({
form: 'ContactForm',
fields,
// validate,
},
mapStateToProps,
)(ContactForm);

View File

@ -1,23 +0,0 @@
@import "utilities";
.form {
padding: $space-sm;
}
.row {
@mixin 2columns 6, 6;
}
.fullRow {
width: 100%;
}
.title {
@mixin fontSize 2;
color: $color-fiord;
font-weight: bold;
}
.submit {
cursor: pointer;
}

View File

@ -1,44 +0,0 @@
import React, { PropTypes } from 'react';
import {
Select,
Input,
} from 'common';
const Emails = ({ accountEmails, fields, onSelectChange }) => {
let emails;
if (accountEmails.length <= 1) {
emails = (
<Input
readOnly
value={accountEmails[0]}
id={'email'}
placeholder="Email"
style={{ marginBottom: '14px', width: '' }}
/>
);
} else {
emails = (
<Select
{...fields.email}
onBlur={() => {}}
placeholder="Email"
style={{ marginBottom: '10px', width: '' }}
options={accountEmails.map((e) => {
return { label: e, value: e };
})}
onChange={onSelectChange}
ignoreCase
clearable={false}
/>
);
}
return emails;
};
Emails.propTypes = {
accountEmails: PropTypes.array.isRequired,
fields: PropTypes.any.isRequired,
onSelectChange: PropTypes.func.isRequired,
};
export default Emails;

View File

@ -1,71 +0,0 @@
import React, { PropTypes } from 'react';
import {
Select,
} from '../../../lib/common';
import css from './styles.css';
const generateMonths = () => {
const months = [];
for (let i = 1; i < 13; i++) {
const num = `0${i}`.slice(-2);
months.push({ label: num, value: num });
}
return months;
};
const generateYears = () => {
const years = [];
const thisYear = new Date().getFullYear();
for (let i = thisYear; i < thisYear + 20; i++) {
years.push({ label: i, value: i });
}
return years;
};
const Expiration = props => {
const { fields, onSelectChange } = props;
const { expMonth, expYear } = fields;
const monthClass = (expMonth.touched && expMonth.error) ?
css.expirationErr : '';
const yearClass = (expYear.touched && expYear.error) ?
css.expirationErr : '';
const error = !!monthClass || !!yearClass ?
(<div className={css.dateErr}>Invalid Expiration</div>) : null;
const accountSelects = (
<div className={css.expiration}>
<div className={css.title}>Expiration Date</div>
<div className={css.dates}>
<Select
{...fields.expMonth}
onBlur={() => {}}
placeholder="Month"
onChange={onSelectChange('expMonth')}
className={monthClass}
options={generateMonths()}
ignoreCase
clearable={false}
/>
<Select
{...fields.expYear}
onBlur={() => {}}
placeholder="Year"
onChange={onSelectChange('expYear')}
className={yearClass}
options={generateYears()}
ignoreCase
clearable={false}
/>
</div>
{ error }
</div>
);
return accountSelects;
};
Expiration.propTypes = {
fields: PropTypes.any.isRequired,
onSelectChange: PropTypes.func.isRequired,
};
export default Expiration;

View File

@ -1,23 +0,0 @@
@import "utilities";
.expiration {
.title {
margin-top: -22px;
padding: $space-xxs;
color: $color-variant-dull;
}
.dates {
@mixin 2columns 6, 6;
margin-bottom: $space-xs;
}
}
.expirationErr > div {
border-color: $color-variant-panic !important;
}
.dateErr {
color: $color-variant-panic;
font-size: $font-size-0;
margin: 0 $space-xxs;
}

View File

@ -1,136 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import isEmpty from 'lodash/isEmpty';
import Expiration from './CardExpiration';
import validate from './validations';
import {
Button,
Input,
} from '../../lib/common';
import css from './styles.css';
const { bool, func, object } = PropTypes;
class PaymentMethodsForm extends Component {
static propTypes = {
billingPaymentError: object,
defaultSelected: bool.isRequired,
fields: object.isRequired,
handleSubmit: func.isRequired,
}
onSelectChange = (field) => (data) => {
const fieldObject = this.props.fields[field];
fieldObject.onChange(data.value);
}
render() {
const {
billingPaymentError: error,
defaultSelected,
fields: propFields,
} = this.props;
const {
cardNumber,
cvv,
firstName,
lastName,
} = propFields;
const errClass = !isEmpty(error) ? css.error : '';
const warning =
'This will update your payment method for all subscriptions';
let submitText = 'Change Card';
if (!defaultSelected) {
submitText = 'Add Card';
}
return (
<div>
<div className={`${css.title} ${errClass}`}>
Credit card information
</div>
<div className={css.form}>
<div className={css.row}>
<Input
{...firstName}
id={'first'}
placeholder="First Name"
errorText={firstName.touched && firstName.error}
/>
<Input
{...lastName}
id={'last'}
placeholder="Last Name"
errorText={lastName.touched && lastName.error}
/>
</div>
<div className={css.cardRow}>
<Input
{...cardNumber}
id={'card'}
placeholder="Card number"
errorText={
(cardNumber.touched && cardNumber.error) ||
(error.type === 'card_error' && error.message)
}
/>
<Input
{...cvv}
id={'cvv'}
placeholder="CVV"
errorText={
(cvv.touched && cvv.error) || error.type === 'card_error'
}
/>
<Expiration
fields={propFields}
onSelectChange={this.onSelectChange}
/>
</div>
<Button
className={css.submit}
type="submit"
onClick={this.props.handleSubmit}
>
{submitText}
</Button>
{warning}
</div>
</div>
);
}
}
const fields = [
'cardNumber',
'cvv',
'firstName',
'lastName',
'expMonth',
'expYear',
];
const mapStateToProps = ({ account, billing }) => {
const {
profiles,
paymentMethods,
} = billing;
const {
namespaceObjects,
selectedNamespace,
} = account;
const selectedUser = namespaceObjects.results[selectedNamespace] || {};
const billingProfile = profiles.results[selectedUser.id];
return {
billingPaymentError: paymentMethods.error,
billingProfile,
};
};
export default reduxForm({
form: 'paymentMethodsForm',
fields,
validate,
},
mapStateToProps,
)(PaymentMethodsForm);

View File

@ -1,49 +0,0 @@
@import "utilities";
.form {
padding: $space-sm;
}
.row {
@mixin 2columns 6, 6;
}
.cardRow {
@mixin 3columns 6, 2, 4;
margin-top: $space-md;
}
.postalRow {
@mixin 3columns 4, 2, 6;
}
.title {
@mixin fontSize 2;
color: $color-fiord;
font-weight: bold;
}
.error {
@mixin fontSize 2;
font-weight: bold;
color: $color-variant-panic;
}
.submit {
cursor: pointer;
margin-top: $space-lg;
}
.warning {
color: $color-variant-panic;
margin
display: flex;
flex-direction: column;
margin: 0 $space-md;
.icon {
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@ -1,47 +0,0 @@
export default (values) => {
const errors = {};
const {
cardNumber,
cvv,
expMonth,
expYear,
firstName,
lastName,
} = values;
if (!firstName) {
errors.firstName = 'Required';
}
if (!lastName) {
errors.lastName = 'Required';
}
if (!cardNumber) {
errors.cardNumber = 'Required';
}
if (!cvv) {
errors.cvv = 'Required';
}
if (!expMonth) {
errors.expMonth = 'Required';
}
if (!expYear) {
errors.expYear = 'Required';
}
const date = new Date();
if (
values.expMonth < (date.getMonth() + 1) &&
values.expYear <= date.getFullYear()
) {
errors.expMonth = 'Required';
errors.expYear = 'Required';
}
/*
TODO: feature - nathan 05/19/16
Luhn card number check.
cvv check.
expiration check.
billing address validations check
email check?
account check? (already has a plan associated)
*/
return errors;
};

View File

@ -1,310 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import map from 'lodash/map';
import isEmpty from 'lodash/isEmpty';
import sortBy from 'lodash/sortBy';
import find from 'lodash/find';
import {
billingCreatePaymentMethod,
billingDeletePaymentMethod,
billingFetchPaymentMethods,
billingSetDefaultPaymentMethod,
billingUpdateProfile,
} from '../actions';
import {
Card,
Button,
FetchingError,
FullscreenLoading,
WarningIcon,
} from '../lib/common';
import {
TINY,
} from '../lib/constants';
import PaymentForm from './PaymentForm';
import ContactForm from './ContactForm';
import css from './styles.css';
const dispatcher = {
billingCreatePaymentMethod,
billingDeletePaymentMethod,
billingFetchPaymentMethods,
billingSetDefaultPaymentMethod,
billingUpdateProfile,
};
const mapStateToProps = ({ billing }) => {
const {
paymentMethods,
} = billing;
return {
paymentMethods,
};
};
const { object, bool, shape, func } = PropTypes;
@connect(mapStateToProps, dispatcher)
export default class PaymentMethodsView extends Component {
static propTypes = {
billingProfile: object.isRequired,
paymentMethods: shape({
isFetching: bool.isRequired,
results: object.isrequired,
}),
selectedUser: object.isRequired,
// actions
billingCreatePaymentMethod: func.isRequired,
billingDeletePaymentMethod: func.isRequired,
billingFetchPaymentMethods: func.isRequired,
billingSetDefaultPaymentMethod: func.isRequired,
billingUpdateProfile: func.isRequired,
}
state = {
selectedCardId: '',
defaultPaymentWarning: '',
}
setDefaultCard = (bfId) => () => {
const {
selectedUser,
billingSetDefaultPaymentMethod: setDefaultPaymentMethod,
billingFetchPaymentMethods: fetchPaymentMethods,
} = this.props;
const { id: docker_id } = selectedUser;
setDefaultPaymentMethod({
card_id: bfId,
docker_id,
}).then(() => {
fetchPaymentMethods({ docker_id });
});
}
addCard = (data, shouldFetch) => {
const {
billingProfile,
selectedUser,
billingCreatePaymentMethod: createPaymentMethod,
billingFetchPaymentMethods: fetchPaymentMethods,
} = this.props;
const { id: docker_id } = selectedUser;
const billforward_id = billingProfile && billingProfile.billforward_id;
const paymentInfo = {
billforward_id,
name_first: data.firstName,
name_last: data.lastName,
cvc: data.cvv,
number: data.cardNumber,
exp_month: data.expMonth,
exp_year: data.expYear,
};
if (!!shouldFetch) {
return createPaymentMethod(paymentInfo).then(() => {
fetchPaymentMethods({ docker_id });
});
}
return createPaymentMethod(paymentInfo);
}
replaceCard = (bfId) => (data) => {
const addCard = this.addCard;
const deleteCard = this.deleteCard;
addCard(data).then(() => {
deleteCard(bfId)();
});
}
deleteCard = (bfId) => () => {
const {
selectedUser,
billingDeletePaymentMethod: deletePaymentMethod,
billingFetchPaymentMethods: fetchPaymentMethods,
} = this.props;
const { id: docker_id } = selectedUser;
deletePaymentMethod({ docker_id, card_id: bfId }).then(() => {
fetchPaymentMethods({ docker_id });
});
}
updateBillingProfile = (values) => {
const {
selectedUser,
billingUpdateProfile: updateProfile,
} = this.props;
const {
address1,
city,
company,
country,
email,
firstName,
job,
lastName,
phone,
postalCode,
province,
} = values;
const docker_id = selectedUser.id;
const address = {
address_line_1: address1,
city,
province,
country,
post_code: postalCode,
primary_address: true,
};
const submitData = {
addresses: [
address,
],
company_name: company,
docker_id,
email,
first_name: firstName,
job_function: job,
last_name: lastName,
phone_primary: phone,
};
updateProfile(submitData).then(() => {
const keys = selectedUser.type === 'User' ? {
dockerUUID: selectedUser.id,
Docker_Hub_User_Name__c: selectedUser.username,
} : {
dockerUUID: selectedUser.id,
Docker_Hub_Organization_Name__c: selectedUser.orgname,
};
analytics.track('account_info_update', {
...keys,
email,
firstName,
lastName,
jobFunction: job,
address: address1,
postalCode,
state: province,
city,
country,
phone,
});
});
}
renderCardInfo = (card, idx) => {
const {
bf_payment_method_id: cardId,
description,
expiry_month,
expiry_year,
card_type,
default: defaultCard,
} = card;
const isDefault = defaultCard ? 'Default' : '';
const endsWith = description.replace(/#*/, 'x');
let selected =
cardId === this.state.selectedCardId ? css.selected : '';
if (!this.state.selectedCardId && defaultCard) {
selected = css.selected;
}
const selectCard = () => { this.setState({ selectedCardId: cardId }); };
return (
<Card
className={`${css.paymentMethod} ${selected}`}
onClick={selectCard}
key={idx}
>
{isDefault}
<div>
{card_type} ending in {endsWith}
</div>
<div>
Expires: {expiry_month}/{expiry_year}
</div>
</Card>
);
}
render() {
// TODO design - nathan make this GORGEOUS. because it's ugly af right now
const {
paymentMethods,
} = this.props;
const {
selectedCardId,
} = this.state;
if (paymentMethods.isFetching) {
return <FullscreenLoading />;
} else if (!isEmpty(paymentMethods.fetchingError)) {
return (
<div className={css.fetchingError}>
<FetchingError resource="your payment methods" />
</div>
);
} else if (paymentMethods.results.length < 1) {
return (
<Card shadow>
No Payment Methods to show
</Card>
);
}
let cardActions;
const defaultCard = find(paymentMethods.results, card => card.default);
let defaultSelected = false;
let paymentFormSubmit = this.addCard;
if (selectedCardId && defaultCard.bf_payment_method_id !== selectedCardId) {
cardActions = (
<div className={css.updateCardRow}>
<div>
<Button
className={css.submit}
onClick={this.setDefaultCard(selectedCardId)}
>
Make Default Card {this.state.hover}
</Button>
<Button
className={css.submit}
onClick={this.deleteCard(selectedCardId)}
>
Delete Payment Method
</Button>
</div>
<div className={css.warning}>
<WarningIcon size={TINY} />&nbsp;
Warning: Changing your default payment method will
change your default payment for ALL subscriptions
</div>
</div>
);
} else if (
defaultCard.bf_payment_method_id === selectedCardId || !selectedCardId
) {
defaultSelected = true;
paymentFormSubmit = this.replaceCard(defaultCard.bf_payment_method_id);
}
const sortedPaymentMethods =
sortBy(paymentMethods.results, method => !method.default);
const splitForms = (
<div className={css.splitForms}>
<PaymentForm
onSubmit={paymentFormSubmit}
defaultSelected={defaultSelected}
/>
<ContactForm onSubmit={this.updateBillingProfile} />
</div>
);
return (
<Card title="Payment Information" shadow>
<div className={css.infoRow}>
<div className={css.title}>Credit Cards</div>
{map(sortedPaymentMethods, this.renderCardInfo)}
</div>
{cardActions}
{splitForms}
</Card>
);
}
}

View File

@ -1,45 +0,0 @@
@import "utilities";
.title {
@mixin fontSize 2;
color: $color-fiord;
font-weight: bold;
}
.updateCardRow {
margin-bottom: $space-md;
}
.warning {
color: $color-variant-panic;
margin
display: flex;
align-items: center;
margin: 0 $space-md;
}
.paymentMethod {
display: inline-flex;
width: 22%;
min-width: 200px;
margin: $space-md;
&.selected {
border: 1px solid $color-variant-primary-hover;
}
&:hover {
border: 1px solid $color-variant-primary-light;
cursor: pointer;
}
}
.splitForms {
@mixin 2columns 6, 6;
> div:first-child {
border-right: 1px solid $color-loblolly;
}
}
.submit {
cursor: pointer;
margin: $space-xs $space-md;
}

View File

@ -1,39 +0,0 @@
import {
billingCreatePaymentMethod,
billingDeletePaymentMethod,
billingFetchInvoices,
billingFetchPaymentMethods,
billingFetchProduct,
billingFetchProfile,
billingFetchProfileSubscriptions,
billingSetDefaultPaymentMethod,
billingUpdateProfile,
} from 'actions/billing';
import {
accountFetchCurrentUser,
accountFetchUser,
accountFetchUserEmails,
accountSelectNamespace,
} from 'actions/account';
import {
repositoryFetchOwnedNamespaces,
} from 'actions/repository';
export default {
accountFetchCurrentUser,
accountFetchUser,
accountFetchUserEmails,
accountSelectNamespace,
billingCreatePaymentMethod,
billingDeletePaymentMethod,
billingFetchInvoices,
billingFetchPaymentMethods,
billingFetchProduct,
billingFetchProfile,
billingFetchProfileSubscriptions,
billingSetDefaultPaymentMethod,
billingUpdateProfile,
repositoryFetchOwnedNamespaces,
};

View File

@ -1,266 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import InvoicesView from './Invoices';
import PaymentMethodsView from './PaymentMethods';
import {
accountFetchCurrentUser,
accountFetchUser,
accountFetchUserEmails,
accountSelectNamespace,
billingFetchInvoices,
billingFetchPaymentMethods,
billingFetchProfile,
repositoryFetchOwnedNamespaces,
} from './actions';
import {
AccountSelect,
Card,
FullscreenLoading,
Tab,
Tabs,
} from './lib/common';
import {
PAYMENTS,
METHODS,
} from './lib/constants';
import css from './styles.css';
const { arrayOf, func, object, string, shape, bool } = PropTypes;
const mapStateToProps = ({ account, billing }) => {
const {
currentUser,
namespaceObjects,
ownedNamespaces,
selectedNamespace,
} = account;
const {
profiles,
} = billing;
return {
billingProfiles: profiles,
currentUser,
namespaceObjects,
ownedNamespaces,
selectedNamespace,
};
};
const dispatcher = {
accountFetchUser,
accountFetchUserEmails,
accountFetchCurrentUser,
accountSelectNamespace,
billingFetchInvoices,
billingFetchPaymentMethods,
billingFetchProfile,
repositoryFetchOwnedNamespaces,
};
@connect(mapStateToProps, dispatcher)
export default class BillingProfile extends Component {
static propTypes={
billingProfiles: shape({
isFetching: bool.isRequired,
results: object.isRequired,
}),
currentUser: object.isRequired,
namespaceObjects: object.isRequired,
// List of namespace strings owned by User
ownedNamespaces: arrayOf(string),
selectedNamespace: string.isRequired,
// Action Props
accountFetchUser: func.isRequired,
accountFetchUserEmails: func.isRequired,
accountFetchCurrentUser: func.isRequired,
accountSelectNamespace: func.isRequired,
billingFetchInvoices: func.isRequired,
billingFetchPaymentMethods: func.isRequired,
billingFetchProfile: func.isRequired,
repositoryFetchOwnedNamespaces: func.isRequired,
};
state = {
currentView: PAYMENTS,
isInitializing: true,
selectedNamespace: '',
}
componentWillMount() {
const {
// actions
accountFetchCurrentUser: fetchCurrentUser,
accountFetchUserEmails: fetchUserEmails,
accountSelectNamespace: selectNamespace,
billingFetchInvoices: fetchInvoices,
billingFetchPaymentMethods: fetchPaymentMethods,
billingFetchProfile: fetchBillingProfiles,
repositoryFetchOwnedNamespaces: fetchOwnedNamespaces,
} = this.props;
Promise.when([
fetchCurrentUser().then((userRes) => {
const { username: namespace, id: docker_id } = userRes.value;
return Promise.when([
fetchInvoices({ docker_id }),
fetchUserEmails({ user: namespace }),
selectNamespace({ namespace }),
fetchBillingProfiles({ docker_id, isOrg: false }).then((res) => {
if (res.value.profile) {
fetchPaymentMethods({ docker_id });
}
}),
]);
}),
fetchOwnedNamespaces(),
]).then(() => {
this.setState({ isInitializing: false });
}).catch(() => {
this.setState({ isInitializing: false });
});
}
onSelectPage = (e, value) => {
this.setState({ currentView: value });
}
onSelectNamespace = ({ value }) => {
const {
currentUser,
namespaceObjects,
// actions
accountFetchUser: fetchUser,
accountSelectNamespace: selectNamespace,
billingFetchPaymentMethods: fetchPaymentMethods,
billingFetchProfile: fetchBillingProfiles,
billingFetchInvoices: fetchInvoices,
} = this.props;
this.setState({ initializing: true });
const fetchedNamespaces = namespaceObjects.results;
if (fetchedNamespaces[value]) {
const userOrg = fetchedNamespaces[value];
const { type, id: docker_id } = userOrg;
const isOrg = type === 'Organization';
return Promise.when([
fetchInvoices({ docker_id }),
selectNamespace({ namespace: value }),
fetchBillingProfiles({ docker_id, isOrg }).then((res) => {
if (res.value.profile) {
fetchPaymentMethods({ docker_id });
}
}),
]).then(() => {
this.setState({
isInitializing: false,
});
}).catch(() => {
this.setState({
isInitializing: false,
});
});
}
const isOrg = value !== currentUser.username;
// fetching user info vs. org info locally hits different api's which
// requires knowing which endpoint to hit.
// unnecessary in production since v2/users will redirect to v2/orgs
return fetchUser({ namespace: value, isOrg }).then((userRes) => {
const { id: docker_id } = userRes.value;
return Promise.when([
fetchInvoices({ docker_id }),
selectNamespace({ namespace: value }),
fetchBillingProfiles({ docker_id, isOrg })
.then((billingRes) => {
if (billingRes.value.profile) {
fetchPaymentMethods({ namespace: value, docker_id });
}
}),
]).then(() => {
this.setState({
isInitializing: false,
});
}).catch(() => {
this.setState({
isInitializing: false,
});
});
});
}
generateSelectOptions(namespaces) {
return namespaces.map((option) => {
return { value: option, label: option };
});
}
render() {
const {
currentView,
isInitializing,
} = this.state;
const {
billingProfiles,
namespaceObjects,
ownedNamespaces,
selectedNamespace,
} = this.props;
const isLoading = namespaceObjects.isFetching || billingProfiles.isFetching;
if (isInitializing || isLoading) {
return <FullscreenLoading />;
}
const selectedUser = namespaceObjects.results[selectedNamespace];
const billingProfile = billingProfiles.results[selectedUser.id];
// IF NAMESPACE HAS NO BILLING PROFILE RENDER EMPTY CARD
if (!billingProfile) {
// TODO: design - nathan 6/8/16 - MAKE PRETTIER NO CONTENT PAGE
return (
<div>
<div className={css.emptyNav}>
<AccountSelect
className={css.select}
options={this.generateSelectOptions(ownedNamespaces)}
onSelectChange={this.onSelectNamespace}
selectedNamespace={selectedNamespace}
/>
</div>
<div className={css.content}>
<Card shadow>No Billing Profile to display</Card>
</div>
</div>
);
}
let content;
if (currentView === PAYMENTS) {
content = <InvoicesView selectedUser={selectedUser} />;
} else if (currentView === METHODS) {
content = (
<PaymentMethodsView
selectedUser={selectedUser}
billingProfile={billingProfile}
/>);
}
return (
<div>
<div className={css.navigation}>
<Tabs
className={css.tabs}
selected={currentView}
onSelect={this.onSelectPage}
>
<Tab value={PAYMENTS}>Billing History</Tab>
<Tab value={METHODS}>Payment Methods</Tab>
</Tabs>
<AccountSelect
className={css.select}
options={this.generateSelectOptions(ownedNamespaces)}
onSelectChange={this.onSelectNamespace}
selectedNamespace={selectedNamespace}
/>
</div>
<div className={css.content}>
{content}
</div>
</div>
);
}
}

View File

@ -1,11 +0,0 @@
export AccountSelect from '../AccountSelect';
export Button from 'common/Button';
export Card from 'common/Card';
export DownloadIcon from 'common/Icon/lib/Download';
export FetchingError from 'common/FetchingError';
export FullscreenLoading from 'common/FullscreenLoading';
export Input from 'common/Input';
export Select from 'common/Select';
export Tab from 'common/Tab';
export Tabs from 'common/Tabs';
export WarningIcon from 'common/Icon/lib/Warning';

View File

@ -1,17 +0,0 @@
const PAYMENTS = 'Payments';
const METHODS = 'Methods';
const TINY = 'tiny';
const SMALL = 'small';
const REGULAR = 'regular';
const LARGE = 'large';
const XLARGE = 'xlarge';
export default {
PAYMENTS,
METHODS,
TINY,
SMALL,
REGULAR,
LARGE,
XLARGE,
};

View File

@ -1,8 +0,0 @@
const countries = require('country-list')();
export const countryOptions = countries.getCodes().map(code => {
const label = countries.getName(code);
return { label, value: code };
});
export const countryGetCodeFromName = name => countries.getCode(name);

View File

@ -1 +0,0 @@
export const noOp = () => {};

View File

@ -1,22 +0,0 @@
@import "utilities";
.navigation {
@mixin 2columns 6, 6;
}
.emptyNav {
display: flex;
justify-content: flex-end;
}
.tabs {
display: flex;
align-items: center;
button {
cursor: pointer;
}
}
.content {
margin: $space-xxl 0;
}

View File

@ -1,88 +0,0 @@
/* eslint-disable max-len */
/* Evaluate DDC on */
export const evaluationInstructions = {
toInstallDDC: {
1: 'Get Docker Toolbox',
2: 'Download license into your project directory',
3: 'Run the following command in the same directory',
},
pullCommand: '$ docker run alexmavr/ddc-in-a-box | bash',
scriptTiming: 'The script will take approximately 15-20 mins depending on your internet connectivity.',
};
// Right side Additional Information
export const additionalInformation = [
{
title: 'System Requirements',
bulletPoints: [
'5GB hard drive space',
'2GB of RAM',
'Internet connectivity (downloads ~4GB)',
'docker-machine & bash (included in Docker Toolbox)',
'Your license file downloaded to a directory on your machine',
],
}, {
title: 'What this container does',
bulletPoints: [
'Creates a VM using docker-machine',
'Installs Universal Control Plane 1.1',
'Installs Trusted Registry 2.0',
'Configures authentication in both systems with a default user',
'Configures licensing in both products',
'Configures UCP to trust the registry service in DTR',
],
}, {
title: 'Advanced Options',
bulletPoints: [
'You can set 3 environment variables for this script, related to the use of docker-machine:',
],
environmentVariables: [
{
variable: 'MACHINE_DRIVER',
description: 'The default is \'virtualbox\'',
}, {
variable: 'MACHINE_DRIVER_FLAGS',
description: 'The default is "--virtualbox-memory 2048 --virtualbox-disk-size 16000"',
}, {
variable: 'MACHINE_NAME',
description: 'The default is = \'ddc-eval\'',
},
],
},
];
/* Deploy on */
export const AWS = {
label: 'AWS using Quickstart',
url: 'https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=DockerDatacenter&templateURL=https://s3-us-west-2.amazonaws.com/ddc-on-aws-public/ddc_on_aws.json',
};
export const Azure = {
label: 'Microsoft Azure from the Marketplace ',
url: 'https://azure.microsoft.com/en-us/marketplace/partners/docker/dockerdatacenterdocker-datacenter/',
};
export const Linux = {
label: 'Linux',
url: 'https://docs.docker.com/docker-trusted-registry/cs-engine/install/',
};
// Links in the right column
export const UCPGuide = {
label: 'Guide for Universal Control Plane',
url: 'https://docs.docker.com/ucp/install-sandbox/',
};
export const DTRGuide = {
label: 'Guide for Docker Trusted Registry',
url: 'https://docs.docker.com/docker-trusted-registry/install/install-dtr/',
};
export const prodInstall = {
label: 'Planning a Production Ready Install',
url: 'https://docs.docker.com/ucp/installation/plan-production-install/',
};

View File

@ -1,185 +0,0 @@
import React, { Component, PropTypes } from 'react';
import {
ClockIcon,
AmazonWebServicesIcon,
AzureIcon,
AppleIcon,
WindowsTextIcon,
LinuxIcon,
BackButtonArea,
CopyPullCommand,
Expand,
} from 'common';
import {
additionalInformation,
AWS,
Azure,
DTRGuide,
evaluationInstructions,
Linux,
prodInstall,
UCPGuide,
} from './constants.js';
import map from 'lodash/map';
import { DDC_ID } from 'lib/constants/eusa';
import css from './styles.css';
const { func } = PropTypes;
export default class DDCInstructions extends Component {
static propTypes = {
showSubscriptionDetail: func.isRequired,
}
showSubscriptionDetail = () => {
this.props.showSubscriptionDetail(DDC_ID);
}
// utility method to generate links
linkTo = ({ url, label }) => {
return <a href={url} className={css.link} target="_blank">{label}</a>;
}
renderEvaluateDDCOn() {
const {
toInstallDDC,
pullCommand,
scriptTiming,
} = evaluationInstructions;
const clock = <ClockIcon className={css.clock} />;
const toolboxLink = 'https://docs.docker.com/toolbox/overview/';
const getDockerLink = this.linkTo({
label: toInstallDDC[1],
url: toolboxLink,
});
const [sysRequirements, scriptDoes, advOptions] = additionalInformation;
const mkBulletPoints = (text, index) => {
return <li key={index} className={css.expandContent}>{text}</li>;
};
const mkEnvVars = ({ variable, description }, index) => {
return (
<li key={index} className={css.expandContentNoBullet}>
<div className={css.envVar}>{variable}</div>
<div>{description}</div>
</li>
);
};
return (
<div className={css.section}>
<div>
<div className={css.sectionTitle}>
<span className={css.semiBold}>{'Option 1: '}</span>
Evaluate a single node on your desktop
</div>
<div>
<AppleIcon className={css.logo} />
<WindowsTextIcon className={css.logo} />
</div>
<div className={css.installSteps}>
<div>1. {getDockerLink}</div>
<div>2. {toInstallDDC[2]}</div>
<div>3. {toInstallDDC[3]}</div>
</div>
<div className={css.pullCommand}>
<CopyPullCommand fullCommand={pullCommand} hasInstruction={false} />
</div>
<div className={css.licenseHelpText}>{clock} {scriptTiming}</div>
</div>
<div>
<div className={css.listTitle}>Additional Information</div>
<div className={css.expandWrapper}>
<Expand title={sysRequirements.title}>
<ul className={css.list}>
{map(sysRequirements.bulletPoints, mkBulletPoints)}
</ul>
</Expand>
</div>
<div className={css.expandWrapper}>
<Expand title={scriptDoes.title}>
<ul className={css.list}>
{map(scriptDoes.bulletPoints, mkBulletPoints)}
</ul>
</Expand>
</div>
<div className={css.expandWrapper}>
<Expand title={advOptions.title}>
<ul className={css.list}>
<li className={css.expandContentNoBullet}>
{advOptions.bulletPoints[0]}
</li>
{map(advOptions.environmentVariables, mkEnvVars)}
</ul>
</Expand>
</div>
</div>
</div>
);
}
renderGuideLink = ({ url, label }) => {
return (
<div key={label} className={css.guideLink}>
{this.linkTo({ url, label })}
</div>
);
}
renderDeployOn = () => {
const guideLinks = [
UCPGuide,
DTRGuide,
prodInstall,
];
return (
<div className={css.section}>
<div>
<div className={css.sectionTitle}>
<span className={css.semiBold}>{'Option 2: '}</span>
Deploy on
</div>
<div className={css.deployBlocks}>
<div className={css.deployBlock}>
<AmazonWebServicesIcon className={css.logo} />
{this.linkTo({ url: AWS.url, label: AWS.label })}
</div>
<div className={css.deployBlock}>
<AzureIcon className={css.logo} />
{this.linkTo({ url: Azure.url, label: Azure.label })}
</div>
<div className={css.deployBlock}>
<LinuxIcon className={css.logo} />
{this.linkTo({ url: Linux.url, label: Linux.label })}
</div>
</div>
</div>
<div>
<div className={css.listTitle}>Resources</div>
{map(guideLinks, this.renderGuideLink)}
</div>
</div>
);
}
render() {
const supportText =
'Having trouble getting setup? Please contact ';
const mailTo = 'mailto:support@docker.com?subject=Docker Datacenter';
const supportLink =
this.linkTo({ url: mailTo, label: 'support@docker.com' });
return (
<div>
<BackButtonArea
onClick={this.showSubscriptionDetail}
text="Docker Datacenter"
/>
<div className={css.title}>
Getting Started with Docker Datacenter
</div>
{this.renderEvaluateDDCOn()}
<hr className={css.hr} />
{this.renderDeployOn()}
<hr className={css.hr} />
<div>{supportText} {supportLink}</div>
</div>
);
}
}

View File

@ -1,119 +0,0 @@
@import "utilities";
.hr {
margin-top: $space-xxl;
margin-bottom: $space-xxl;
}
.title {
@mixin fontSize 6;
margin-bottom: $space-sm;
}
.link {
text-decoration: underline;
cursor: pointer;
display: inline-block;
color: $color-fiord;
}
.semiBold {
@mixin semiBold;
}
/* must scale svgs with inline styles/width/height set */
.logo {
height: $icon-size-xlarge !important;
width: auto !important;
margin-bottom: $space-md;
}
.logo + .logo {
margin-left: $space-xxl;
}
.section {
@mixin 2columns 8, 4;
@mixin fontSize 2;
}
.sectionTitle {
margin-bottom: $space-xxl;
}
.listTitle {
@mixin fontSize 3;
margin-bottom: $space-md;
}
/* Evaluating */
.installSteps {
margin-bottom: $space-lg;
}
.pullCommand {
width: 60%;
margin-bottom: $space-sm;
}
.licenseHelpText {
@mixin fontSize 0;
display: flex;
align-items: center;
color: $color-regent-gray;
margin-bottom: $space-sm;
}
.clock {
@mixin square $space-md;
fill: $color-regent-gray;
margin-right: $space-xs;
}
/* Additional Information */
$ul-padding: $space-xxxl;
$li-padding: $space-sm;
.expandWrapper {
margin-bottom: $space-lg;
}
.expandContent {
color: $color-regent-gray;
margin-bottom: $space-md;
padding-left: $li-padding;
}
.expandContentNoBullet {
composes: expandContent;
/* Hide the bullet point */
display: inherit;
}
.list {
padding-left: $ul-padding;
}
.envVar {
text-transform: uppercase;
color: $color-fiord;
}
/* Deploy and Resources */
.guideLink {
margin-bottom: $space-md;
}
.deployBlocks {
display: flex;
align-items: flex-end;
}
.deployBlock {
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
margin-right: $space-xxxxl;
}

View File

@ -1,587 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import {
BackButtonArea,
Button,
Card,
CheckIcon,
CloseIcon,
CopyPullCommand,
DownloadIcon,
ImageWithFallback,
Markdown,
Modal,
} from 'common';
import { FALLBACK_IMAGE_SRC, FALLBACK_ELEMENT } from 'lib/constants/fallbacks';
import filter from 'lodash/filter';
import find from 'lodash/find';
import map from 'lodash/map';
import startCase from 'lodash/startCase';
// Blob is a polyfill to support older browsers
require('lib/utils/blob');
import { saveAs } from 'lib/utils/file-saver';
import moment from 'moment';
import classnames from 'classnames';
import getLogo from 'lib/utils/get-largest-logo';
import { SMALL } from 'lib/constants/sizes';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import { isPullableProduct } from 'lib/utils/product-utils';
import { DOCKER, DOCKER_GUID } from 'lib/constants/defaults';
import { DDC_TRIAL_PLAN, DDC_ID } from 'lib/constants/eusa';
import { ACTIVE, CANCELLED } from 'lib/constants/states/subscriptions';
import {
billingDeleteSubscription,
billingFetchLicenseFile,
billingFetchProfileSubscriptions,
} from 'actions/billing';
import css from './styles.css';
const { arrayOf, bool, func, object, shape, string } = PropTypes;
// Has a subscription been created within the past day?
const isNewSubscription = (subscription) => {
const YESTERDAY = moment().subtract(1, 'days').startOf('day');
return moment(subscription.initial_period_start).isAfter(YESTERDAY);
};
const isExpiredLicense = (expiration) => {
return moment().isAfter(expiration);
};
const dispatcher = {
deleteSubscription: billingDeleteSubscription,
fetchLicenseFile: billingFetchLicenseFile,
fetchSubscriptions: billingFetchProfileSubscriptions,
};
@connect(null, dispatcher)
export default class SubscriptionDetail extends Component {
static propTypes = {
deleteInfo: shape({
error: string,
isDeleting: bool,
subscription_id: string,
}),
deleteSubscription: func.isRequired,
error: string,
fetchLicenseFile: func.isRequired,
fetchSubscriptions: func.isRequired,
licenses: shape({
isFetching: bool,
// Licenses are keyed off of subscription_id
results: object,
error: string,
}),
selectedNamespace: string.isRequired,
selectedProductSubscriptions: arrayOf(object).isRequired,
showDDCInstructions: func.isRequired,
showSubscriptionList: func.isRequired,
selectedUserOrOrg: object.isRequired,
}
state = {
cancelSubscriptionId: '',
cancelSubscriptionName: '',
showCancelConfirmation: false,
downloadLicenseSubscriptionId: '',
downloadLicenseError: false,
}
hideCancelConfirmation = () => {
this.setState({
cancelSubscriptionId: '',
cancelSubscriptionName: '',
showCancelConfirmation: false,
});
}
showCancelConfirmation = ({ subscription_id, subscription_name }) => () => {
this.setState({
cancelSubscriptionId: subscription_id,
cancelSubscriptionName: subscription_name,
showCancelConfirmation: true,
});
}
cancelSubscription = () => {
const { cancelSubscriptionId: subscription_id } = this.state;
const {
deleteSubscription,
fetchSubscriptions,
selectedUserOrOrg,
} = this.props;
deleteSubscription({ subscription_id }).then(() => {
fetchSubscriptions({ docker_id: selectedUserOrOrg.id });
});
}
downloadLicense = ({
subscription_id,
subscription_name,
product_rate_plan,
}) => () => {
const { fetchLicenseFile } = this.props;
this.setState({
downloadLicenseSubscriptionId: subscription_id,
downloadLicenseError: false,
}, () => {
fetchLicenseFile({ subscription_id })
.then((res) => {
// value contains the API response
const downloadContent = JSON.stringify(res.value);
this.setState({ downloadLicenseSubscriptionId: '' });
const blob = new Blob([downloadContent], {
type: 'text/plain;charset=utf-8',
});
saveAs(blob, `${subscription_name}.lic`, true);
analytics.track('download_license', {
license_tier: product_rate_plan,
});
})
.catch(() => {
this.setState({ downloadLicenseError: true });
});
});
}
// Returns true if the given subscription can be cancelled, false otherwise.
//
// A subscription may not be cancelled if:
// 1. The initiator does not own the subscription OR
// 2. The subscription is already in a cancelled state OR
// 3. This is an 'offline' subscription.
// Note that the presence of a subscription origin implies that this
// is an offline subscription.
canCancelSubscription(subscription) {
const { isOwner } = this.props.selectedUserOrOrg;
const { state, origin } = subscription;
if (!isOwner) {
return false;
}
if (state === CANCELLED) {
return false;
}
if (origin) {
return false;
}
return true;
}
renderProductSubscriptionSummary = (subs) => {
// Subs is an array containing one or more subscriptions for the
// same product id. When rendering the *product* information (and
// aggregated subscription info), we must look at at least one subscription
// to understand the product information. There is guaranteed to be at least
// one sub, so we can get the information out of the first one.
if (!subs || !subs.length) {
return null;
}
const {
label: productName,
links,
publisher_id,
publisher,
} = subs[0];
const numActiveSubscriptions =
filter(subs, ({ state }) => state === ACTIVE).length;
let partnerName;
let dockerPublisherID = DOCKER_GUID;
if (isStaging() || isDev()) {
dockerPublisherID = DOCKER;
}
if (publisher_id !== dockerPublisherID) {
partnerName = (
<div className={css.partnerName}>
<CheckIcon className={css.check} size={SMALL} />
{publisher.name}
</div>
);
}
const addS = numActiveSubscriptions === 1 ? '' : 's';
const licenses = (
<span>{`${numActiveSubscriptions} active subscription${addS}`}</span>
);
let licenseAgreement;
// Add license agreement if it exists in the links
if (links) {
const link = find(links, ({ label }) => label === 'License Agreement');
if (link) {
const { label: text, url } = link;
licenseAgreement =
<span> | <a href={url} target="_blank">{text}</a></span>;
}
}
return (
<div>
<div className={css.product}>
<div className={css.productName}>{productName}</div>
{partnerName}
</div>
<div className={css.nameAndLicense}>
{this.props.selectedNamespace} | {licenses} {licenseAgreement}
</div>
</div>
);
}
renderCancelSubscriptionLine(subscription) {
const { isDeleting, subscription_id: deletingId } = this.props.deleteInfo;
const { subscription_name, subscription_id } = subscription;
if (!this.canCancelSubscription(subscription)) {
return null;
}
const isDeletingThisSub = isDeleting && deletingId === subscription_id;
if (isDeletingThisSub) {
return <div>Canceling Subscription...</div>;
}
const subInfo = { subscription_id, subscription_name };
return (
<div
className={css.underlined}
onClick={this.showCancelConfirmation(subInfo)}
>
Cancel Subscription
</div>
);
}
renderPullCommand(subscription) {
// TODO Kristie 8/17/19 Fix this when we actually set up the new
// subscriptions page (and per sub pull commands)
// This is a known issue until then
const {
plans: productPlans,
product_rate_plan: currentRatePlanID,
} = subscription;
// TODO Kristie 8/17/19 This needs to be synced with billing
let currentPlanDetail = find(productPlans, ({ id }) => {
return id === currentRatePlanID;
});
// TODO Kristie 8/17/19 Remove this when billing plan ids are in sync with
// the product plan ids
if (!currentPlanDetail) {
currentPlanDetail = productPlans[0];
}
const { repositories, default_version } = currentPlanDetail;
// Assumption: there is only one repo per rate plan - this may change in the
// future
if (!repositories || !repositories[0]) {
return null;
}
const { namespace, reponame } = repositories[0];
return (
<CopyPullCommand
codeClassName={css.pullText}
namespace={namespace}
reponame={reponame}
tag={default_version && default_version.linux}
/>
);
}
renderPricingComponents(subscription) {
const {
pricing_components,
// The specific rate plan that this subscription is subscribed to
product_rate_plan: sub_rate_plan,
// All possible rate plans and their pricing components for this product
rate_plans,
} = subscription;
// Find the rate plan that matches this subscription's rate plan
const plan = find(rate_plans, (rp) => rp.name === sub_rate_plan);
// Do not show this user cancelled subscriptions if they are not an owner
if (!plan || !pricing_components || !pricing_components.length) {
return null;
}
const comps = pricing_components.map((pc) => {
// Find the pricing component (with full information) from the billing
// plan which matches this subscription's pricing component
const priceCompInfo = find(plan.pricing_components, comp => {
// Comp is the full information from billing api, pc is this sub's
// data with the amount purchased (value)
return comp === pc.name;
});
// Fallback to this subscription's component "name"
const label = priceCompInfo && priceCompInfo.label || pc.name;
return `${pc.value} ${label}`;
});
return (
<div>
<div>{plan.label}</div>
<div>{comps.join(', ')}</div>
</div>
);
}
renderDownloadLicense(subscription) {
const { downloadLicenseError, downloadLicenseSubscriptionId } = this.state;
const { subscription_id } = subscription;
const isDownloadingThisLicense =
subscription_id === downloadLicenseSubscriptionId;
const downloadError = isDownloadingThisLicense && downloadLicenseError;
let text = 'License Key';
if (isDownloadingThisLicense && !downloadError) {
text = 'Downloading License...';
} else if (isDownloadingThisLicense && downloadError) {
text = 'Error Downloading License';
}
const icon = <DownloadIcon size={SMALL} />;
const classes = classnames({
[css.downloadLicense]: true,
[css.downloadLicenseError]: isDownloadingThisLicense && downloadError,
});
return (
<div
className={classes}
onClick={this.downloadLicense(subscription)}
>
{icon} {text}
</div>
);
}
renderLicense(subscription) {
const { isOwner } = this.props.selectedUserOrOrg;
const { subscription_id, eusa, state, product_rate_plan } = subscription;
const { results } = this.props.licenses;
// Find this license in results
const license = results[subscription_id];
// Do not render a license for a cancelled subscription
if (!license || state === CANCELLED) {
return null;
}
const { expiration } = license;
const isLicenseExpired = isExpiredLicense(expiration);
const isLegalAccepted = eusa && eusa.accepted;
let licenseLine;
if (!isLegalAccepted) {
const text = isOwner ? 'Accept terms to download'
: 'Admin must accept terms to download';
licenseLine = <div>{text}</div>;
} else if (isLicenseExpired) {
licenseLine = <div>Expired License</div>;
} else {
licenseLine = this.renderDownloadLicense(subscription);
}
let expirationDate;
// TODO Kristie 6/10/16 Incorporate other non-recurring subscriptions
if (product_rate_plan === DDC_TRIAL_PLAN) {
expirationDate = <div>Expires {moment(expiration).format('l')}</div>;
}
return (
<div>
{expirationDate}
{licenseLine}
</div>
);
}
// A single subscription for this product
renderSubscriptionLine = (subscription) => {
const { subscription_name, subscription_id, state } = subscription;
const { isOwner } = this.props.selectedUserOrOrg;
if (!isOwner && state === CANCELLED) {
return null;
}
// TODO Kristie 6/1/16 Include edit sub name
// TODO Kristie 6/1/16 Include purchased by or registered
return (
<div key={subscription_id}>
<hr />
<div className={css.subscriptionLine} >
<div>
<div className={css.subscriptionName}>{subscription_name}</div>
{this.renderPricingComponents(subscription)}
{this.renderCancelSubscriptionLine(subscription)}
</div>
<div className={css.licenseInformation}>
<div className={css.licenseStatus}>{startCase(state)}</div>
{this.renderLicense(subscription)}
</div>
</div>
</div>
);
}
renderNewSubscriptionNotification() {
const { selectedProductSubscriptions: subs } = this.props;
if (!subs || !subs.length) {
return null;
}
const { name, label } = subs[0];
// TODO Get hint from API for paid subscription or not
const isProductPaid = name === 'devddc';
const newSubscriptions = filter(subs, (s) => isNewSubscription(s));
if (!newSubscriptions.length) {
return null;
}
let header;
const details = 'Your subscription details are below.';
if (isProductPaid) {
header = `Payment Successful! Thank you for purchasing ${label}`;
} else {
header = `Thank you for subscribing to ${label}`;
}
return (
<Card className={css.newSubscription} shadow>
<div className={css.notificationHeader}>{header}</div>
{details}
</Card>
);
}
renderErrorNotification() {
// TODO Kristie 6/2/16 Error dismissal
const { error } = this.props.deleteInfo;
if (!error) {
return null;
}
return (
<Card className={css.error} shadow>
<div className={css.notificationHeader}>Error</div>
{['Sorry, we could not cancel your subscription.',
'Please try again.'].join(' ')}
</Card>
);
}
renderCancelSubscriptionModal = () => {
const {
cancelSubscriptionId,
cancelSubscriptionName,
showCancelConfirmation,
} = this.state;
const { isDeleting, subscription_id: deletingId } = this.props.deleteInfo;
const isDeletingThisSub = cancelSubscriptionId === deletingId && isDeleting;
return (
<Modal
isOpen={showCancelConfirmation}
className={css.modal}
onRequestClose={this.hideCancelConfirmation}
>
<div
className={css.closeModal}
onClick={this.hideCancelConfirmation}
>
<CloseIcon size={SMALL} />
</div>
<div className={css.cancelHeader}>
{`Cancel ${cancelSubscriptionName}?`}
</div>
<div className={css.cancelDetail}>
This action cannot be undone.
</div>
<Button onClick={this.cancelSubscription}>
{isDeletingThisSub ? 'Cancelling...' : 'Cancel Subscription'}
</Button>
</Modal>
);
}
renderDDCInstructions() {
const setupText = 'To install DDC on your machine, VM, or cloud instance ';
const instructionsLink = (
<div className={css.link} onClick={this.props.showDDCInstructions}>
follow these instructions
</div>
);
const supportText =
'If you need assistance with your subscription please contact';
const mailTo = 'mailto:support@docker.com?subject=Docker Datacenter';
const supportLink = (
<a href={mailTo} className={css.link}>{'support@docker.com'}</a>
);
return (
<div>
<div className={css.instructionsSection}>
<div className={css.instructionsHeader}>Setup Guide</div>
<div className={css.instructionsDetail}>
{setupText} {instructionsLink}
</div>
</div>
<div className={css.instructionsSection}>
<div className={css.instructionsHeader}>Customer Support</div>
<div className={css.instructionsDetail}>
{supportText} {supportLink}
</div>
</div>
</div>
);
}
renderInstructions() {
const { selectedProductSubscriptions: subs } = this.props;
// At least one subscription otherwise render would have returned null
const { product_id, instructions } = subs[0];
if (product_id === DDC_ID) {
return this.renderDDCInstructions();
}
if (!instructions) {
return null;
}
return (
<div>
<Markdown rawMarkdown={instructions} />
</div>
);
}
renderSubscriptionHeader = (subs) => {
const sampleSub = subs[0];
const { logo_url, product_id } = sampleSub;
const src = getLogo(logo_url);
let pullCommand;
if (isPullableProduct(product_id)) {
pullCommand = this.renderPullCommand(sampleSub);
}
return (
<div className={css.summaryAndPullCommand}>
<div className={css.subscriptionSummary}>
<ImageWithFallback
src={src}
className={css.icon}
fallbackImage={FALLBACK_IMAGE_SRC}
fallbackElement={FALLBACK_ELEMENT}
/>
{this.renderProductSubscriptionSummary(subs)}
</div>
{pullCommand}
</div>
);
}
render() {
const {
selectedProductSubscriptions: subs,
showSubscriptionList,
} = this.props;
if (!subs || !subs.length) {
return null;
}
const { product_id } = subs[0];
const maybeNewSubNotification = this.renderNewSubscriptionNotification();
const maybeErrorNotication = this.renderErrorNotification();
const maybeCancelSubscriptionModal = this.renderCancelSubscriptionModal();
const maybeInstructions = this.renderInstructions();
return (
<div>
<BackButtonArea onClick={showSubscriptionList} text="Subscriptions" />
<Card key={product_id} className={css.subscriptionDetailCard} shadow>
{maybeNewSubNotification}
{maybeErrorNotication}
{this.renderSubscriptionHeader(subs)}
{map(subs, this.renderSubscriptionLine)}
{maybeCancelSubscriptionModal}
</Card>
{maybeInstructions}
</div>
);
}
}

View File

@ -1,178 +0,0 @@
@import "utilities";
$card-padding: 64px;
.product {
display: flex;
align-items: center;
margin-bottom: $space-xs;
}
.icon {
@mixin storeIcon;
margin-right: $space-xl;
}
.nameAndLicense {
@mixin fontSize 2;
color: $color-regent-gray;
}
.subscriptionSummary,
.partnerName {
display: flex;
align-items: center;
}
.summaryAndPullCommand {
@mixin 2columnsResponsive 7, 5;
}
.pullText {
@mixin fontSize 1;
}
.productName {
@mixin fontSize 4;
@mixin semiBold;
margin-right: $space-xs;
}
.check {
fill: $color-robins-egg-blue;
margin-right: $space-xxs;
}
.marketplaceIcon {
fill: $color-regent-gray;
margin-left: $space-xxs;
}
.subscriptionDetailCard {
padding: $card-padding;
margin-bottom: $space-xxl;
}
.subscriptionLine {
@mixin fontSize 2;
color: $color-regent-gray;
display: flex;
align-items: center;
justify-content: space-between;
padding-top: calc($space-xl - 1em);
padding-bottom: calc($space-xl - 1em);
}
.subscriptionName,
.licenseStatus {
@mixin fontSize 3;
color: $color-fiord;
}
.subscriptionName {
@mixin semiBold;
}
.licenseInformation {
text-align: right;
}
.downloadLicense {
@mixin fontSize 2;
display: flex;
font-weight: bold;
color: $color-dodger-blue;
align-items: center;
justify-content: flex-end;
cursor: pointer;
& > svg {
fill: $color-dodger-blue;
margin-right: $space-xxs;
}
}
.downloadLicenseError {
color: $color-carnation;
& > svg {
fill: $color-carnation;
}
}
.newSubscription {
background-color: $color-robins-egg-blue;
box-shadow: 0 2px 4px 0 $color-regent-gray;
margin-bottom: $space-xxxl;
color: $color-white;
position: relative;
}
.error {
background-color: $color-carnation;
box-shadow: 0 2px 4px 0 $color-regent-gray;
margin-bottom: $space-xxxl;
color: $color-white;
position: relative;
}
.notificationHeader {
font-weight: 600;
margin-bottom: $space-sm;
}
.closeModal {
position: absolute;
top: $space-md;
right: $space-md;
}
.close {
fill: $color-white;
}
.modal {
background: $color-white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.cancelHeader {
@mixin fontSize 5;
margin-bottom: $space-sm;
}
.cancelDetail {
@mixin fontSize 3;
color: $color-regent-gray;
margin-bottom: $space-xl;
}
.underlined {
text-decoration: underline;
cursor: pointer;
}
/* DDC Instructions */
.instructionsSection {
padding-right: $card-padding;
padding-left: $card-padding;
margin-bottom: $space-xxl;
}
.instructionsHeader {
@mixin fontSize 3;
@mixin semiBold;
margin-bottom: $space-xl;
}
.instructionsDetail {
@mixin fontSize 2;
}
.link {
composes: underlined;
display: inline-block;
color: $color-fiord;
}

View File

@ -1,317 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import {
Button,
Card,
CheckIcon,
ImageWithFallback,
FullscreenLoading,
FetchingError,
Select,
} from 'common';
import { FALLBACK_IMAGE_SRC, FALLBACK_ELEMENT } from 'lib/constants/fallbacks';
import css from './styles.css';
import {
filter,
find,
get,
map,
sortBy,
} from 'lodash';
import getLogo from 'lib/utils/get-largest-logo';
import { SMALL } from 'lib/constants/sizes';
import { PRIMARY, PANIC } from 'lib/constants/variants';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import { DOCKER, DOCKER_GUID } from 'lib/constants/defaults';
import { ACTIVE, CANCELLED } from 'lib/constants/states/subscriptions';
import { EUSA_LINK } from 'lib/constants/eusa';
import {
billingFetchProfileSubscriptions,
billingUpdateSubscription,
BILLING_BASE_URL,
} from 'actions/billing';
const noOp = () => {};
const { array, bool, func, shape, string, object } = PropTypes;
const dispatcher = {
fetchSubscriptions: billingFetchProfileSubscriptions,
updateSubscription: billingUpdateSubscription,
};
@connect(null, dispatcher)
export default class SubscriptionList extends Component {
static propTypes = {
currentUserNamespace: string,
dockerSubscriptions: array.isRequired,
isFetching: bool,
fetchSubscriptions: func.isRequired,
namespaceObjects: shape({
isFetching: bool,
results: object,
error: string,
}).isRequired,
onSelectNamespace: func.isRequired,
partnerSubscriptions: array.isRequired,
selectedNamespace: string.isRequired,
showSubscriptionDetail: func.isRequired,
updateInfo: object,
updateSubscription: func.isRequired,
selectedUserOrOrg: object,
error: string,
}
onSubscriptionClick = (productId) => () => {
this.props.showSubscriptionDetail(productId);
}
acceptEusa = ({ subscription_id, product_id }) => () => {
const payload = { eusa: { accepted: true } };
const { id: docker_id } = this.props.selectedUserOrOrg;
this.props.updateSubscription({ subscription_id, body: payload })
.then(() => {
// Fetch all the subscriptions so you have the most updated
this.props.fetchSubscriptions({ docker_id }).then(() => {
// Transition to the detail page for this product to download license
this.props.showSubscriptionDetail(product_id);
});
});
}
// Returns the EUSA url associated with the given subscription.
determineEUSA(subscription) {
const { origin } = subscription;
// Use a partner specific EUSA where applicable.
// TODO - This will be obtained from the product catalog in the near future.
if (origin === 'hpe') {
return `${BILLING_BASE_URL}/eusa/hpe/eusa.pdf`;
}
// Use the standard Docker software EUSA
return EUSA_LINK;
}
renderEusaAcceptance(subs) {
const {
namespaceObjects,
selectedNamespace,
updateInfo,
} = this.props;
// All subscriptions that require legal acceptance need to have the legal
// terms accepted
const { eusa_acceptance_required, product_id } = subs[0];
const isOwner =
get(namespaceObjects, ['results', selectedNamespace, 'isOwner']);
if (!eusa_acceptance_required || !isOwner) {
return null;
}
// See if you have to acccept terms for any subscriptions
const subscriptionToAccept = find(subs, ({ eusa, state }) => {
return eusa && !eusa.accepted && state !== CANCELLED;
});
// All subscriptions have accepted terms --> no action needed
if (!subscriptionToAccept) {
return null;
}
const { subscription_id } = subscriptionToAccept;
// Prevent clicking on the buttons here from clicking on the parent div
const stopProp = (e) => {
e.stopPropagation();
};
// Grab that subscription's isUpdating and error information
const { error, isUpdating } = updateInfo[subscription_id] || {};
let buttonText = 'Accept Terms';
if (isUpdating && !error) {
buttonText = 'Accepting Terms...';
} else if (error) {
buttonText = 'Error! Please Try Again';
}
const eusaLink = this.determineEUSA(subscriptionToAccept);
// We only have to cover the EUSA link because all trials will be created
// through the online form and will have accepted eval terms
return (
<div className={css.noClick} onClick={stopProp}>
To access your License keys, please accept the
<a href={eusaLink} target="_blank" className={css.terms}>
Terms of Service
</a>
<Button
onClick={this.acceptEusa({ product_id, subscription_id })}
variant={error ? PANIC : PRIMARY}
>
{buttonText}
</Button>
</div>
);
}
renderProductSubscriptionSummary = (subs) => {
if (!subs || !subs.length) {
return null;
}
const {
publisher_id,
publisher = {},
label,
} = subs[0];
const numActiveSubscriptions =
filter(subs, ({ state }) => state === ACTIVE).length;
let partnerName;
let dockerPublisherID = DOCKER_GUID;
if (isStaging() || isDev()) {
dockerPublisherID = DOCKER;
}
if (publisher_id !== dockerPublisherID) {
partnerName = (
<div className={css.partnerName}>
<CheckIcon className={css.check} size={SMALL} />
{publisher.name}
</div>
);
}
const addS = numActiveSubscriptions === 1 ? '' : 's';
const licenses = (
<span>{`${numActiveSubscriptions} active subscription${addS}`}</span>
);
return (
<div>
<div className={css.product}>
<div className={css.productName}>{label}</div>
{partnerName}
</div>
<div className={css.nameAndLicense}>
{licenses}
</div>
</div>
);
}
renderProductSubscriptions = (subs) => {
// Subscription is an array containing one or more subscriptions for the
// same product id
const { product_id, logo_url } = subs[0];
const src = getLogo(logo_url);
const maybeEusaAcceptance = this.renderEusaAcceptance(subs);
return (
<Card
key={product_id}
className={css.subscriptionCard}
hover
shadow
onClick={this.onSubscriptionClick(product_id)}
>
<div className={css.subscriptionSummary}>
<ImageWithFallback
className={css.icon}
fallbackElement={FALLBACK_ELEMENT}
fallbackImage={FALLBACK_IMAGE_SRC}
src={src}
/>
{this.renderProductSubscriptionSummary(subs)}
</div>
<div>{maybeEusaAcceptance}</div>
</Card>
);
}
renderNoSubscriptions(type) {
return (
<Card shadow>You have no {type} subscriptions.</Card>
);
}
renderPartnerSubscriptionList() {
const { partnerSubscriptions } = this.props;
let subs = map(partnerSubscriptions, this.renderProductSubscriptions);
if (!partnerSubscriptions.length) {
return null;
}
return (
<div>
<div className={css.sectionTitle}>Docker Partner Services</div>
<div>{subs}</div>
</div>
);
}
renderDockerSubscriptionList() {
const { dockerSubscriptions } = this.props;
let subs = map(dockerSubscriptions, this.renderProductSubscriptions);
if (!dockerSubscriptions.length) {
subs = this.renderNoSubscriptions('Docker');
}
return (
<div>
<div className={css.dockerSectionTitle}>
<div className={css.sectionTitle}>Docker Services</div>
<div>{this.renderNamespaceSelect()}</div>
</div>
<div>{subs}</div>
</div>
);
}
renderNamespaceSelect() {
const { namespaceObjects, currentUserNamespace } = this.props;
let options = map(namespaceObjects.results, (userObj) => {
const namespace = userObj.username || userObj.orgname;
return { value: namespace, label: namespace };
});
if (!namespaceObjects.results[currentUserNamespace]) {
// current user should be included in namespace objects
// BUT if it's not, we should add it to the front
options.unshift(
{ value: currentUserNamespace, label: currentUserNamespace }
);
}
options = sortBy(options, (option) => {
return option.value !== currentUserNamespace;
});
return (
<div className={css.namespaceSelectWrapper}>
Account
<Select
className={css.namespaceSelect}
clearable={false}
ignoreCase
onBlur={noOp}
onChange={this.props.onSelectNamespace}
options={options}
placeholder="Account"
value={this.props.selectedNamespace}
/>
</div>
);
}
render() {
const {
isFetching,
error,
} = this.props;
if (isFetching) {
return <FullscreenLoading />;
} else if (error) {
return (
<div>
<div className={css.empty}>
{this.renderNamespaceSelect()}
</div>
<div className={css.fetchingError}>
<FetchingError resource="your subscriptions" />
</div>
</div>
);
}
return (
<div>
{this.renderDockerSubscriptionList()}
{this.renderPartnerSubscriptionList()}
</div>
);
}
}

View File

@ -1,96 +0,0 @@
@import "utilities";
.product {
display: flex;
align-items: center;
margin-bottom: $space-xs;
}
.icon {
@mixin storeIcon;
margin-right: $space-xl;
}
.nameAndLicense {
@mixin fontSize 2;
color: $color-regent-gray;
}
.sectionTitle {
@mixin title;
padding: $space-xl 0;
}
.dockerSectionTitle {
display: flex;
justify-content: space-between;
align-items: center;
}
.partnerName {
display: flex;
align-items: center;
}
.productName {
@mixin fontSize 4;
@mixin semiBold;
margin-right: $space-xs;
}
.check {
fill: $color-robins-egg-blue;
margin-right: $space-xxs;
}
.marketplaceIcon {
fill: $color-regent-gray;
margin-left: $space-xxs;
}
.subscriptionCard {
cursor: pointer;
margin-bottom: $space-sm;
& > div {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.subscriptionSummary {
display: flex;
align-items: center;
}
.namespaceSelect {
height: $select-height;
width: $select-width;
margin-left: $space-sm;
}
.namespaceSelectWrapper {
@mixin fontSize 2;
color: $color-regent-gray;
display: flex;
align-items: center;
}
.terms {
margin-left: $space-xxs;
margin-right: $space-sm;
}
.noClick {
height: 100%;
}
.fetchingError {
padding-top: $space-xl;
color: $color-regent-gray;
}
.empty {
display: flex;
justify-content: flex-end;
}

View File

@ -1,11 +0,0 @@
import map from 'lodash/map';
export const setIsOwnerParam = (fetchedNamespaces, ownedNamespaces) => {
const results = {};
map(fetchedNamespaces, (userOrg) => {
const username = userOrg.username || userOrg.orgname;
const isOwner = ownedNamespaces.indexOf(username) >= 0;
results[username] = { ...userOrg, isOwner };
});
return results;
};

View File

@ -1,365 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import {
FullscreenLoading,
} from 'common';
import forEach from 'lodash/forEach';
import get from 'lodash/get';
import isStaging from 'lib/utils/isStaging';
import isDev from 'lib/utils/isDevelopment';
import { DOCKER, DOCKER_GUID } from 'lib/constants/defaults';
import SubscriptionList from './SubscriptionList';
import SubscriptionDetail from './SubscriptionDetail';
import DDCInstructions from './DDCInstructions';
import { setIsOwnerParam } from './helpers';
import css from './styles.css';
import { isProductBundle } from 'lib/utils/product-utils';
import {
CANCELLED,
DDC_INSTRUCTIONS,
SUBSCRIPTION_DETAIL,
SUBSCRIPTION_LIST,
} from 'lib/constants/states/subscriptions';
import {
accountFetchCurrentUser,
accountFetchUser,
accountFetchUserOrgs,
accountSelectNamespace,
} from 'actions/account';
import {
billingFetchLicenseDetail,
billingFetchProduct,
billingFetchProfileSubscriptionsAndProducts,
} from 'actions/billing';
import { repositoryFetchOwnedNamespaces } from 'actions/repository';
import {
marketplaceFetchBundleDetail,
marketplaceFetchRepositoryDetail,
} from 'actions/marketplace';
const {
array,
arrayOf,
bool,
func,
object,
objectOf,
string,
shape,
} = PropTypes;
const mapStateToProps = ({ account, billing, marketplace }) => {
const {
currentUser,
// object of all fetched namespace objects (owned & orgs)
namespaceObjects,
ownedNamespaces,
selectedNamespace,
} = account;
const currentUserNamespace = currentUser.username || '';
// Set the isOwner property for each user / org object in results
namespaceObjects.results =
setIsOwnerParam(namespaceObjects.results, ownedNamespaces);
// Is this user an owner for the _currently selected_ namespace?
const isOwner =
get(namespaceObjects, ['results', selectedNamespace, 'isOwner']);
const { subscriptions, products: billingProducts } = billing;
const { bundles, images } = marketplace;
const {
delete: deleteInfo,
error,
isFetching,
results,
update: updateInfo,
} = subscriptions;
const dockerSubscriptions = [];
const partnerSubscriptions = [];
const subscriptionsByProductId = {};
if (!isFetching) {
let dockerPublisherID = DOCKER_GUID;
if (isStaging() || isDev()) {
dockerPublisherID = DOCKER;
}
// Group each subscription by product
forEach(results, (subscription) => {
// Look up the product id associated with this subscription id
const { product_id, state } = subscription;
if (!isOwner && state === CANCELLED) {
// Non owners cannot view cancelled subscriptions
return;
}
// Get the billing product info for that product (rate_plans, etc.)
const billingProductInfo = billingProducts[product_id];
// Get the product catalog information for that product (ex. logo, name)
let productCatalogInfo;
if (isProductBundle(product_id)) {
productCatalogInfo = bundles[product_id] || {};
} else {
productCatalogInfo = images.certified[product_id] || {};
}
// If we CANNOT get the billing product or product catalog information,
// do NOT display this subscription
const productErr = productCatalogInfo && productCatalogInfo.error;
const billingErr = billingProductInfo && billingProductInfo.error;
if (productErr || billingErr) {
return;
}
// Combine the subscription info and product info collision on `name`)
const allInfo = {
// Subscription info and billing product info both have name
subscription_name: subscription.name,
// Product catalog & subscription both have eusa (we want subscription)
...productCatalogInfo,
...billingProductInfo,
...subscription,
// TODO Kristie 8/17/16 Separate this info better so that there are no
// collisions
name: productCatalogInfo.name,
};
// Check if this product exists in the subscriptions already
const existingSub = subscriptionsByProductId[product_id];
// Add this subscription + product info to the list of subscriptions for
// this product, or create a list if none exists
const newSubs = existingSub ? [...existingSub, allInfo] : [allInfo];
subscriptionsByProductId[product_id] = newSubs;
});
// Separate each product (and its array of subscriptions) by vendor
forEach(subscriptionsByProductId, (productSubs) => {
// Guaranteed to be at least one subscription for this product
const { publisher_id } = productSubs[0];
if (publisher_id === dockerPublisherID) {
dockerSubscriptions.push(productSubs);
} else {
partnerSubscriptions.push(productSubs);
}
});
}
return {
currentUserNamespace,
deleteInfo,
dockerSubscriptions,
error,
isFetching,
licenses: subscriptions && subscriptions.licenses,
namespaceObjects,
partnerSubscriptions,
selectedNamespace,
subscriptionsByProductId,
updateInfo,
};
};
const dispatcher = {
accountFetchCurrentUser,
accountFetchUser,
accountFetchUserOrgs,
accountSelectNamespace,
billingFetchLicenseDetail,
billingFetchProduct,
billingFetchProfileSubscriptionsAndProducts,
marketplaceFetchBundleDetail,
marketplaceFetchRepositoryDetail,
repositoryFetchOwnedNamespaces,
};
/*
* Subscriptions is the connected component that handles showing either
* a SubscriptionList or a SubscriptionDetail page. Subscriptions is only
* shown within the context of the MagicCarpet by clicking on the Subscriptions
* link in the Nav dropdown. There is no route within the app.
*/
@connect(mapStateToProps, dispatcher)
export default class Subscriptions extends Component {
static propTypes = {
accountFetchCurrentUser: func.isRequired,
accountFetchUser: func.isRequired,
accountFetchUserOrgs: func.isRequired,
accountSelectNamespace: func.isRequired,
billingFetchLicenseDetail: func.isRequired,
billingFetchProduct: func.isRequired,
billingFetchProfileSubscriptionsAndProducts: func.isRequired,
marketplaceFetchBundleDetail: func.isRequired,
marketplaceFetchRepositoryDetail: func.isRequired,
repositoryFetchOwnedNamespaces: func.isRequired,
currentUserNamespace: string,
error: string,
// Object of all fetched namespace objects keyed off of namespace
namespaceObjects: shape({
isFetching: bool,
results: object,
error: string,
}).isRequired,
isFetching: bool,
selectedNamespace: string,
// Values are arrays containing subscription(s) for a product (key)
subscriptionsByProductId: objectOf(array),
// The following props are not being used in this component
// but being passed down through to it's children
deleteInfo: object,
// Array of arrays containing subscription(s) for a docker product
dockerSubscriptions: arrayOf(array),
licenses: shape({
isFetching: bool,
// Licenses are keyed off of subscription_id
results: object,
error: string,
}),
// Array of arrays containing subscription(s) for a product
partnerSubscriptions: arrayOf(array),
updateInfo: object,
}
state = {
// View list of subscriptions (grouped by product) or a detail view of all
// subscriptions for a selected product
currentView: SUBSCRIPTION_LIST,
isInitializing: true,
selectedProductId: '',
}
componentWillMount() {
const {
accountFetchCurrentUser: fetchCurrentUser,
accountFetchUserOrgs: fetchUserOrgs,
accountSelectNamespace: selectNamespace,
billingFetchProfileSubscriptionsAndProducts: fetchProfileSubscriptions,
repositoryFetchOwnedNamespaces: fetchOwnedNamespaces,
} = this.props;
Promise.when([
fetchOwnedNamespaces(),
fetchUserOrgs(),
fetchCurrentUser().then((res) => {
const { username: namespace, id: docker_id } = res.value;
Promise.all([
fetchProfileSubscriptions({ docker_id }),
selectNamespace({ namespace }),
]);
}).catch(() => { this.setState({ isInitializing: false }); }),
]).then(() => {
this.setState({
isInitializing: false,
});
}).catch(() => {
this.setState({ isInitializing: false });
});
}
onSelectNamespace = ({ value: namespace }) => {
const {
accountFetchUser: fetchUser,
accountSelectNamespace: selectNamespace,
currentUserNamespace,
billingFetchProfileSubscriptionsAndProducts: fetchProfileSubscriptions,
namespaceObjects,
} = this.props;
// NOTE
// currently all options are being populated from already fetched namespaces
// BUT just in case - this will fetch the namespaceObject
if (!namespaceObjects.results[namespace]) {
const isOrg = namespace !== currentUserNamespace;
return fetchUser({ namespace, isOrg }).then((userRes) => {
const { id: docker_id } = userRes.value;
Promise.all([
selectNamespace({ namespace }),
fetchProfileSubscriptions({ docker_id }),
]);
});
}
const { id } = namespaceObjects.results[namespace];
return Promise.all([
selectNamespace({ namespace }),
fetchProfileSubscriptions({ docker_id: id }),
]);
}
showSubscriptionDetail = (productId) => {
this.setState({
currentView: SUBSCRIPTION_DETAIL,
selectedProductId: productId,
});
}
showSubscriptionList = () => {
this.setState({ currentView: SUBSCRIPTION_LIST });
}
showDDCInstructions = () => {
this.setState({ currentView: DDC_INSTRUCTIONS });
}
generateSelectOptions(namespaceObjects) {
const namespaceArray = [];
forEach(namespaceObjects, (val, key) => {
namespaceArray.push({ value: key, label: key });
});
return namespaceArray;
}
render() {
const {
currentView,
isInitializing,
selectedProductId,
} = this.state;
const {
currentUserNamespace,
deleteInfo,
dockerSubscriptions,
error,
isFetching,
licenses,
namespaceObjects,
partnerSubscriptions,
selectedNamespace,
subscriptionsByProductId,
updateInfo,
} = this.props;
const selectedUserOrOrg = namespaceObjects.results[selectedNamespace];
const selectedProductSubs = subscriptionsByProductId[selectedProductId];
let content;
if (isInitializing || isFetching) {
content = <FullscreenLoading />;
} else if (currentView === SUBSCRIPTION_LIST) {
content = (
<SubscriptionList
currentUserNamespace={currentUserNamespace}
selectedUserOrOrg={selectedUserOrOrg}
dockerSubscriptions={dockerSubscriptions}
namespaceObjects={namespaceObjects}
onSelectNamespace={this.onSelectNamespace}
partnerSubscriptions={partnerSubscriptions}
selectedNamespace={selectedNamespace}
showSubscriptionDetail={this.showSubscriptionDetail}
updateInfo={updateInfo}
error={error}
/>
);
} else if (currentView === DDC_INSTRUCTIONS) {
content = (
<DDCInstructions
showSubscriptionDetail={this.showSubscriptionDetail}
/>
);
} else {
content = (
<SubscriptionDetail
selectedUserOrOrg={selectedUserOrOrg}
deleteInfo={deleteInfo}
error={error}
licenses={licenses}
selectedNamespace={selectedNamespace}
selectedProductSubscriptions={selectedProductSubs}
showDDCInstructions={this.showDDCInstructions}
showSubscriptionList={this.showSubscriptionList}
/>
);
}
return (
<div className={css.content}>
{content}
</div>
);
}
}

View File

@ -1,5 +0,0 @@
@import "utilities";
.content {
margin: $space-xxl 0;
}

View File

@ -1,122 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { Input, Button } from 'components/common';
import css from './styles.css';
class AdminHomeCreateRepositoryForm extends Component {
static propTypes = {
fields: PropTypes.object.isRequired,
submitting: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
}
render() {
const {
fields: {
display_name,
namespace,
reponame,
publisher,
},
submitting,
handleSubmit,
} = this.props;
const repoInputStyle = { width: '200px' };
const inputStyle = { width: '256px' };
return (
<form onSubmit={handleSubmit}>
<div>
<Input
id={'display_name_input'}
placeholder="Display Name"
style={inputStyle}
errorText={display_name.touched && display_name.error}
{...display_name}
/>
</div>
<div>
store&nbsp;/&nbsp;<Input
id={'namespace_input'}
errorText={namespace.touched && namespace.error}
placeholder="namespace"
style={repoInputStyle}
{...namespace}
/>&nbsp;/&nbsp;<Input
id={'reponame_input'}
errorText={reponame.touched && reponame.error}
placeholder="reponame"
style={repoInputStyle}
{...reponame}
/>
</div>
<div>
<Input
id={'publisher_id_input'}
placeholder="Publisher Docker ID"
style={inputStyle}
errorText={publisher.id.touched && publisher.id.error}
{...publisher.id}
/>
</div>
<div>
<Input
id={'publisher_name_input'}
placeholder="Publisher Name"
style={inputStyle}
errorText={
publisher.name.touched && publisher.name.error
}
{...publisher.name}
/>
</div>
<div className={css.buttons}>
<Button type="submit" disabled={submitting}>Add Repository</Button>
</div>
</form>
);
}
}
export default reduxForm({
form: 'adminHomeCreateRepository',
fields: [
'display_name',
'namespace',
'reponame',
'publisher.id',
'publisher.name',
],
validate: values => {
const errors = {};
if (!values.display_name) {
errors.display_name = 'Required';
}
if (!values.namespace) {
errors.namespace = 'Required';
} else if (!/^[a-z0-9_]+$/.test(values.namespace)) {
errors.namespace = 'Invalid namespace';
}
if (!values.reponame) {
errors.reponame = 'Required';
} else if (!/^[a-zA-Z0-9-_.]+$/.test(values.reponame)) {
errors.reponame = 'Invalid reponame';
}
errors.publisher = {};
if (!values.publisher.id) {
errors.publisher.id = 'Required';
} else if (!/^[a-z0-9_]+$/.test(values.publisher.id)) {
errors.publisher.id = 'Invalid Publisher ID';
}
if (!values.publisher.name) {
errors.publisher.name = 'Required';
}
return errors;
},
})(AdminHomeCreateRepositoryForm);

View File

@ -1,4 +0,0 @@
.buttons {
display: flex;
justify-content: flex-end;
}

View File

@ -1,176 +0,0 @@
import React, { PropTypes } from 'react';
import { Table,
TableBody,
TableHeader,
TableHeaderColumn,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import {
Link,
} from 'react-router';
import {
Button,
Modal,
Card,
} from 'components/common';
import {
marketplaceCreateRepository,
marketplaceDeleteRepository,
} from 'actions/marketplace';
import { billingCreateProduct } from 'actions/billing';
import { connect } from 'react-redux';
import routes from 'lib/constants/routes';
import AdminHomeCreateRepositoryForm from './AdminHomeCreateRepositoryForm';
import get from 'lodash/get';
import css from './styles.css';
const { func, object, array } = PropTypes;
const mapStateToProps = ({ marketplace }, { params }) => {
const summaries = get(marketplace, ['search', 'pages', 1, 'results'], []);
return {
params,
summaries,
};
};
const mapDispatch = {
billingCreateProduct,
marketplaceCreateRepository,
marketplaceDeleteRepository,
};
@connect(mapStateToProps, mapDispatch)
export default class Admin extends React.Component {
static propTypes = {
params: object,
summaries: array,
billingCreateProduct: func,
marketplaceCreateRepository: func,
marketplaceDeleteRepository: func,
}
static contextTypes = {
router: object.isRequired,
}
state = { isModalOpen: false }
handleSubmit = (values) => {
const defaultValues = {
categories: [],
platforms: [],
default_version: {},
};
this.props.marketplaceCreateRepository({
...defaultValues,
...values,
source: 'official',
}).then(res => {
const id = res.value.product_id;
return this.props.billingCreateProduct({ id, body: {
name: `${values.namespace}-${values.reponame}`,
label: values.display_name,
publisher_id: values.publisher.id,
rate_plans: [{
name: 'free',
label: 'Free',
duration: 1,
duration_period: 'year',
trial: 0,
trial_period: 'none',
currency: 'USD',
}],
} }).then(() => {
this.context.router.push(routes.adminRepository({ id }));
});
}).catch(err => {
// TODO (jmorgan): show error text in form if creating a product failed
console.log(err);
});
}
openModal = () => {
this.setState({ isModalOpen: true });
}
closeModal = () => {
this.setState({ isModalOpen: false });
}
deleteRepository = (id) => () => {
const result = confirm('Are you sure you want to delete this repository?');
if (result) {
this.props.marketplaceDeleteRepository({ id });
}
};
render() {
return (
<div>
<div className={css.header}>
<h2>{this.props.summaries.length} Repositories</h2>
<div className={css.new}>
<div className={css.buttonwrapper}>
<Button onClick={this.openModal} className={css.nomargin}>
Add Repository
</Button>
</div>
</div>
</div>
<Table selectable={false}>
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
<TableRow>
<TableHeaderColumn>Name</TableHeaderColumn>
<TableHeaderColumn>Publisher</TableHeaderColumn>
<TableHeaderColumn>Repository</TableHeaderColumn>
<TableHeaderColumn className={css.smallColumn}>
Status
</TableHeaderColumn>
<TableHeaderColumn className={css.smallColumn} />
<TableHeaderColumn className={css.smallColumn} />
</TableRow>
</TableHeader>
<TableBody displayRowCheckbox={false}>
{this.props.summaries.map(s => {
return (
<TableRow key={s.id}>
<TableRowColumn>{s.display_name}</TableRowColumn>
<TableRowColumn>{s.publisher.name}</TableRowColumn>
<TableRowColumn className={css.monospace}>
store/{s.namespace}/{s.reponame}
</TableRowColumn>
<TableRowColumn className={css.smallColumn}>
LIVE
</TableRowColumn>
<TableRowColumn className={css.smallColumn}>
<Link to={routes.adminRepository({ id: s.id })}>
Edit
</Link>
</TableRowColumn>
<TableRowColumn className={css.smallColumn}>
<a href="#" onClick={this.deleteRepository(s.id)}>
Delete ×
</a>
</TableRowColumn>
</TableRow>
);
})}
</TableBody>
</Table>
<Modal
isOpen={this.state.isModalOpen}
onRequestClose={this.closeModal}
className={css.modal}
>
<Card title="Add Repository to the Store">
<AdminHomeCreateRepositoryForm
onSubmit={this.handleSubmit}
/>
</Card>
</Modal>
</div>
);
}
}

View File

@ -1,29 +0,0 @@
.header {
display: flex;
flex-direction: row;
margin-top: 10px;
}
.new {
flex: 1 0;
display: flex;
justify-content: flex-end;
align-items: center;
.nomargin {
margin: 0;
}
}
.modal {
padding: 0;
width: 600px !important;
}
.monospace {
font-family: monospace;
}
.smallColumn {
width: 110px !important;
}

View File

@ -1,353 +0,0 @@
const request = require('superagent-promise')(require('superagent'), Promise);
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { DOWNLOAD_ATTRIBUTES_READABLE } from 'lib/constants/eusa';
import { Input, Button, LabelField, Checkbox } from 'components/common';
import uuid from 'uuid-v4';
import css from './styles.css';
import { jwt } from 'lib/utils/authHeaders';
class AdminRepositoryEditMetadataForm extends Component {
static propTypes = {
categories: PropTypes.object.isRequired,
platforms: PropTypes.object.isRequired,
fields: PropTypes.object.isRequired,
submitting: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
}
onChangeCategory = (name) => (e) => {
let categories = this.props.fields.categories.value || [];
if (e.target.checked) {
categories.push({ name, label: this.props.categories[name] });
} else {
categories = categories.filter(c => c.name !== name);
}
this.props.fields.categories.onChange(categories);
}
onChangePlatform = (name) => (e) => {
let platforms = this.props.fields.platforms.value || [];
if (e.target.checked) {
platforms.push({ name, label: this.props.platforms[name] });
} else {
platforms = platforms.filter(c => c.name !== name);
}
this.props.fields.platforms.onChange(platforms);
}
onChangePreviewFile = (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
const extension = file.name.split('.').pop();
if (extension !== 'png') {
console.err('Need png extension');
return;
}
request
.get('/api/ui/admin/signed-s3-put')
.set(jwt())
.accept('application/json')
.query({
'file-name':
`${this.props.fields.id.value}-${uuid()}-logo_large.${extension}`,
'file-type': file.type,
})
.end()
.then((res) => {
const data = JSON.parse(res.text);
return request
.put(data.signedRequest)
.set('Content-Type', file.type)
.send(file)
.end().then(() => {
this.props.fields.logo_url.large.onChange(data.url);
});
}).catch(err => {
// TODO (jmorgan): show error text in form if creating a product failed
console.log(err);
});
}
onChangeScreenshotFile = (screenshot) => (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
const extension = file.name.split('.').pop();
if (extension !== 'png') {
console.err('Need png extension');
return;
}
request
.get('/api/ui/admin/signed-s3-put')
.accept('application/json')
.query({
'file-name':
// eslint-disable-next-line
`${this.props.fields.id.value}-${uuid()}-screenshot_large.${extension}`,
'file-type': file.type,
})
.end()
.then((res) => {
const data = JSON.parse(res.text);
return request
.put(data.signedRequest)
.set('Content-Type', file.type)
.send(file)
.end().then(() => {
screenshot.url.onChange(data.url);
});
}).catch(err => {
// TODO (jmorgan): show error text in form if creating a product failed
console.log(err);
});
}
render() {
const {
fields: {
display_name,
namespace,
reponame,
publisher,
short_description,
full_description,
categories,
platforms,
logo_url,
screenshots,
links,
eusa,
download_attribute,
instructions,
},
submitting,
handleSubmit,
} = this.props;
return (
<form onSubmit={handleSubmit} className={css.repositoryform}>
<LabelField label="Display Name">
<Input
disabled
id={'display_name_input'}
placeholder="Display Name"
{ ...display_name}
/>
</LabelField>
<LabelField label="Store Repository">
<div className={css.monospace}>
store&nbsp;/&nbsp;
<Input
disabled
id={'namespace_input'}
placeholder="namespace"
{ ...namespace }
/>
&nbsp;/&nbsp;
<Input
disabled
id={'reponame_input'}
placeholder="reponame"
{ ...reponame }
/>
</div>
</LabelField>
<LabelField label="Publisher Docker ID">
<Input
disabled
id={'publisher_id_input'}
placeholder="Publisher Docker ID"
{ ...publisher.id }
/>
</LabelField>
<LabelField label="Publisher Name">
<Input
disabled
id={'publisher_name_input'}
placeholder="Publisher Name"
{ ...publisher.name }
/>
</LabelField>
<LabelField label="Entitlement">
<select
{ ...download_attribute }
value={download_attribute.value || ''}
>
<option></option>
{Object.keys(DOWNLOAD_ATTRIBUTES_READABLE).map(k => (
<option key={k} value={k}>
{DOWNLOAD_ATTRIBUTES_READABLE[k]}
</option>
))}
</select>
</LabelField>
<LabelField label="Logo">
<div className={css.logo}>
<img
alt=""
className={css.preview}
src={logo_url.large.value}
/>
<input
id={'logo-input'}
type="file"
onChange={this.onChangePreviewFile}
/>
<label htmlFor="logo-input">{'Size must be 256x256 pixels'}</label>
</div>
</LabelField>
<LabelField label="Screenshots">
<div>
{!screenshots.length && <div>No Screenshots</div>}
{screenshots.map((screenshot, index) =>
<div key={index}>
<h3>Screenshot {index + 1}</h3>
<div>
<div className={css.screenshotwrapper}>
<img
alt=""
className={css.screenshotpreview}
src={screenshot.url.value}
/>
</div>
<Input
id={`screenshot-input-${index}`}
type="text"
placeholder="Label"
{ ...screenshot.label }
/>&nbsp;&nbsp;&nbsp;<input
id={index}
type="file"
onChange={this.onChangeScreenshotFile(screenshot)}
/>
<label htmlFor="index">
Size must be 1920 by 1200 pixels or larger
</label>
</div>
</div>
)}
<div>
<Button type="button" onClick={() => screenshots.addField()}>
Add Screenshot
</Button>
</div>
</div>
</LabelField>
<LabelField label="Short Description">
<textarea { ...short_description} />
</LabelField>
<LabelField className={css.fulldescription} label="Full Description">
<textarea { ...full_description} />
</LabelField>
<LabelField label="Instructions">
<textarea placeholder="" { ...instructions } />
</LabelField>
<LabelField label="End-User License Agreement">
<textarea placeholder="" { ...eusa } />
</LabelField>
<LabelField label="Links">
<div>
{!links.length && <div>No Links</div>}
{links.map((link, index) =>
<div key={index}>
<div>
<Input
id={`links-label-input-${index}`}
type="text"
placeholder="Label"
{ ...link.label }
/>
&nbsp;&nbsp;&nbsp;
<Input
id={`links-url-input-${index}`}
type="text"
placeholder="https://..."
{ ...link.url }
/>
</div>
</div>
)}
<div>
<Button type="button" onClick={() => links.addField()}>
Add Link
</Button>
</div>
</div>
</LabelField>
<LabelField label="Categories">
<div>
{Object.keys(this.props.categories).map(k =>
<Checkbox
key={k}
label={this.props.categories[k]}
onCheck={this.onChangeCategory(k)}
checked={categories.value ?
categories.value.filter(c => c.name === k).length > 0 :
false
}
/>
)}
</div>
</LabelField>
<LabelField label="Platforms">
<div>
{Object.keys(this.props.platforms).map(k =>
<Checkbox
key={k}
label={this.props.platforms[k]}
onCheck={this.onChangePlatform(k)}
checked={platforms.value ?
!!platforms.value.filter(p => p.name === k).length :
false
}
/>
)}
</div>
</LabelField>
<div className={css.savebutton}>
<Button disabled={submitting} className={css.nomargin}>
Save
</Button>
</div>
</form>
);
}
}
export default reduxForm({
form: 'adminRepositoryEditMetadataForm',
fields: [
'id',
'display_name',
'namespace',
'reponame',
'publisher.id',
'publisher.name',
'short_description',
'full_description',
'logo_url.large',
'screenshots[].label',
'screenshots[].url',
'links[].label',
'links[].url',
'eusa',
'download_attribute',
'instructions',
// TODO (jmorgan): change this to use x[].y redux-form syntax
'categories',
'platforms',
],
validate: () => {
const errors = {};
return errors;
},
})(AdminRepositoryEditMetadataForm);

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