+ {createRepoFormStore.fields.name.error +
+ ' No spaces and special characters other than - and . are allowed. Repo names should not begin/end with a . or - .'}
+
+ );
+ descriptionClass = 'large-12 columns form-error';
+ }
+ var defaultValue = createRepoFormStore.values.is_private ? 'private' : 'public';
+ var errText = '';
+ if (createRepoFormStore.fields.is_private.hasError) {
+ errText =
+
+ {createRepoFormStore.fields.is_private.error}
+ ;
+ }
+ const subtitleContent = `1. Choose a namespace *(Required)*
+2. Add a repository name *(Required)*
+3. Add a short description
+4. Add markdown to the full description field
+5. Set it to be a private or public repository`;
+
+ return (
+ {subtitleContent}}>
+
+ This will be the base wrapper of the 'Users' page where either your or another users profile will appear
+ This will let you see your public facing page at /u/username/ too
+ 'Your' homepage/dashboard will live at /home/
+
+
+
+ This is root user page.
+ When not looking at a specific user or an owned image
+ This will show a list of repos/images owned by the root user
+ This could be a image box of some sort
+
+ );
+ }
+});
+
+var User = React.createClass({
+ // This page should either be the users home page or the view page for other users
+ render: function() {
+ return (
+
+
+ This is the UID: {this.props.params.uid}
+ This is main user page.
+ This will show a list of repos/images owned by the user
+
+
+
+ Organizations can have multiple Teams. Teams can have differing permissions. Namespace is
+ unique and this is where repositories for this organization will be created.
+ }>
+
+
+
+ );
+ }
+});
+
+export default connectToStores(AddOrganizationForm,
+ [
+ AddOrganizationStore
+ ],
+ function({ getStore }, props) {
+ return getStore(AddOrganizationStore).getState();
+ });
diff --git a/app/scripts/components/account/BillingPlans.css b/app/scripts/components/account/BillingPlans.css
new file mode 100644
index 0000000000..d0d01aa909
--- /dev/null
+++ b/app/scripts/components/account/BillingPlans.css
@@ -0,0 +1,21 @@
+@import 'dux/css/colors.css';
+
+.body {
+ margin-top: 2rem;
+}
+
+.error {
+ background: var(--primary-5);
+ color:white;
+}
+
+.plansQuestion {
+ margin-bottom: 2rem;
+}
+
+.questionTitle {
+ font-weight: 500;
+ color: var(--secondary-1);
+}
+
+/*.questionAnswer {}*/
diff --git a/app/scripts/components/account/BillingPlans.jsx b/app/scripts/components/account/BillingPlans.jsx
new file mode 100644
index 0000000000..dc241415cc
--- /dev/null
+++ b/app/scripts/components/account/BillingPlans.jsx
@@ -0,0 +1,249 @@
+/*global MktoForms2*/
+
+'use strict';
+
+import React, { PropTypes } from 'react';
+const { string, array, object, func, shape } = PropTypes;
+import { Link } from 'react-router';
+import connectToStores from 'fluxible-addons-react/connectToStores';
+import isEmpty from 'lodash/lang/isEmpty';
+
+import BillingPlansStore from '../../stores/BillingPlansStore';
+import updateSubscriptionPlanOrPackage from '../../actions/updateSubscriptionPlanOrPackage.js';
+
+import PlansTable from './billingplans/Plans';
+import EnterpriseSubscriptions from './billingplans/EnterpriseSubscriptions.jsx';
+import BillingInfo from './billingplans/BillingInfo.jsx';
+import InvoiceTables from './billingplans/InvoiceTables.jsx';
+import styles from './BillingPlans.css';
+import { FullSection } from '../common/Sections.jsx';
+import Route404 from '../common/RouteNotFound404Page.jsx';
+import { PageHeader } from 'dux';
+import DocumentTitle from 'react-document-title';
+
+/* Marketo constants for the marketing survey form */
+const mktoFormId = 1317;
+const mktoFormElemId = 'mktoForm_' + mktoFormId;
+const mktoFormBaseUrl = 'https://app-sj05.marketo.com';
+const mktoFormMunchkinId = '929-FJL-178';
+
+var BillingInfoPage = React.createClass({
+
+ contextTypes: {
+ executeAction: func.isRequired
+ },
+
+ PropTypes: {
+ JWT: string,
+ user: object,
+ currentPlan: shape({
+ id: string,
+ plan: string,
+ package: string
+ }),
+ accountInfo: shape({
+ account_code: string,
+ username: string,
+ email: string,
+ first_name: string,
+ last_name: string,
+ company_name: string
+ }),
+ billingInfo: shape({
+ city: string,
+ state: string,
+ zip: string,
+ first_name: string,
+ last_name: string,
+ address1: string,
+ address2: string,
+ country: string
+ }),
+ plansError: string,
+ invoices: array,
+ unsubscribing: string,
+ updatePlan: string
+ },
+
+ stopSubscription(subscriptionType) {
+ /**
+ * UPDATE 4/6/16 "cloud_metered" is the new "free" plan instead of deleting
+ * per ticket HUB-2219
+ */
+ return () => {
+ const { JWT, user, currentPlan } = this.props;
+ const namespace = user.username || user.orgname;
+ let subscriptionData = {
+ JWT,
+ username: namespace,
+ subscription_uuid: currentPlan.subscription_uuid
+ };
+ if (subscriptionType === 'plan') {
+ subscriptionData.plan_code = 'cloud_metered';
+ if (currentPlan.package) {
+ // preserve package (like cloud_starter) if it exists
+ subscriptionData.package_code = currentPlan.package;
+ }
+ } else if (subscriptionType === 'package') {
+ // If you are removing a package, leave the plan alone
+ subscriptionData.plan_code = currentPlan.plan;
+ // Explicitly set null to remove
+ subscriptionData.package_code = null;
+ }
+ this.context.executeAction(updateSubscriptionPlanOrPackage, subscriptionData);
+ };
+ },
+
+ showSurveyModal(subscriptionType) {
+ return () => {
+ if (typeof MktoForms2 === 'object' &&
+ typeof MktoForms2.loadForm === 'function') {
+ MktoForms2.loadForm(
+ mktoFormBaseUrl,
+ mktoFormMunchkinId,
+ mktoFormId,
+ (form) => {
+ form.onSubmit(this.stopSubscription(subscriptionType));
+ // Don't refresh the page after a successful submission.
+ // React component will re-render itself.
+ form.onSuccess(() => false);
+ form.vals({'Email': this.props.accountInfo.email});
+ MktoForms2.lightbox(form).show();
+ });
+ } else {
+ // If for any reason there is a problem with the Marketo script,
+ // we shouldn't block the user from stopping his/her subscription.
+ this.stopSubscription(subscriptionType);
+ }
+ };
+ },
+
+ getSurveyModalHtml() {
+ return (
+
+ The Docker Hub Registry is free to use for public repositories. Plans with private repositories are
+ available in different sizes. All plans allow collaboration with unlimited people.
+
+
+ );
+ plansFooter = (
+
+
+
What types of payment do you accept?
+
Credit card (Visa, MasterCard, Discover, or American Express).
+
+
+
Do I have to pay to use your service?
+
No, you only have to pay if you require one or more private repository.
+
+
+
Can I change my plan at a later time?
+
Yes, you can upgrade or downgrade at any time.
+
+
+
What if I need a larger plan?
+
Please contact our Sales team at sales@docker.com or call us toll free at 888-214-4258.
Your user account will be transformed into an organization account where all administrative duties are left to another user or group of users. You will no longer be able to login to this account.
+
+
+
Email addresses for this account will be removed, freeing them up to be used for any other accounts.
+
+
+
Converting your account removes any associations to other services like GitHub or Atlassian Bitbucket. You will be able to link your external accounts to another Docker Hub user.
+
+
+
Billing details and Private Repository plans will remain attached to this account after it is converted to an organization.
+
+
+
Repository namespaces and names remain unchanged. Any user collaborators that you have configured for these repositories will be removed and must be reconfigured using group collaborators.
+
+
+
Automated Builds for this account will be updated to appear as if they were originallly configured by the initial organization owner. Any user in a group with 'admin' level access to a repository will be able to edit Automated Build Configurations.
+
+
+
+
WARNING
+
This account conversion operation can not be undone.
+
+
+
+
+
In order to complete the conversion of your account to an organization you will need to enter the Docker ID of an **existing** Docker Hub user account.
+ The user account you specify will become a member of the Owners group and will have full administrative privileges to manage the organization.
+
+ These account links are currently used for Automated Builds,
+ so that we can access your project lists and help you configure your Automated Builds.
+ Please note: A github/bitbucket account can be connected to only one docker hub account at a time.}>
+
+ );
+ }
+});
+
+function mkPage(pageNumber) {
+ return (
+
+ );
+}
+
+export default createClass({
+ displayName: 'Pagination',
+ propTypes: {
+ next: PropTypes.string,
+ prev: PropTypes.string,
+ currentPage: PropTypes.number.isRequired,
+ pageSize: PropTypes.number.isRequired,
+ onChangePage: PropTypes.func.isRequired
+ },
+ _onClick(pageNumber) {
+ return (e) => {
+ //Check if currentPage is to the right of ellipsis and the last from the beginning or the end
+ //based on whether it is the beginning side or end side, update the page ranges
+ e.preventDefault();
+ this.props.onChangePage(pageNumber);
+ };
+ },
+ render() {
+ var paginationComponent;
+ // is there a page before this one?
+ var previousPageExists = !!this.props.prev;
+ // is there a page after this one?
+ var nextPageExists = !!this.props.next;
+ var currentPage = [this.props.currentPage].map(mkPage, this);
+ var prevClasses = classnames({
+ 'arrow': true,
+ 'unavailable': !previousPageExists
+ });
+
+ var nextClasses = classnames({
+ 'arrow': true,
+ 'unavailable': !nextPageExists
+ });
+
+ var prevPage = null;
+ if (previousPageExists) {
+ prevPage = [(
+
No matching repositories for '{this.state.currentQuery}'.
+
+ );
+ } else {
+ return ;
+ }
+ },
+ renderReposList() {
+ const { paginationMode, reposList } = this.state;
+ const { isOwner, repos } = this.props;
+
+ return ;
+ },
+ componentWillReceiveProps(nextProps) {
+ //We assume that loading filter results will be true only on click of the filter
+ //And, the next time, we receive new props we set it to false
+ //Also, we don't call the API after we get into filter mode, the user is stuck with filter mode after he clicks on filter
+ //We also, would go back to pagination on click out and back
+ if (nextProps.STATUS === SUCCESSFUL) {
+ this.setState({
+ reposList: nextProps.repos,
+ currentQuery: ''
+ });
+ }
+
+ //If the context changes and the dashboard goes into an org context
+ //This route will have a param only when there is an Org dashboard loading -> /u//
+ //We will set the reposList to the nextProps in this scenario and also clear the query
+ const { params } = this.props;
+ const orgnameParam = params.user;
+ if (orgnameParam && orgnameParam !== this.state.orgName) {
+ this.setState({
+ orgName: orgnameParam,
+ reposList: nextProps.repos,
+ paginationMode: true,
+ currentQuery: ''
+ });
+ }
+ },
+ render() {
+
+ const {
+ next,
+ prev,
+ repos,
+ STATUS,
+ params,
+ location,
+ currentUserContext
+ } = this.props;
+
+ let namespace = params.user;
+ var content = (
+
+
+
+
+
+ );
+ var currentPageNumber = parseInt(location.query ? location.query.page : 1, 10);
+ var maybePagination;
+ if (repos && repos.length > 0 && this.state.paginationMode) {
+ maybePagination = (
+
+
+
+
+
+ );
+ }
+
+ //A filter bar on top of the repositories list, when we are doing client side filtering
+ let maybeMessage;
+ var showTotal = 0;
+ var showCount = 0;
+ if (repos) {
+ showTotal = repos.length;
+ }
+ if (this.state.reposList) {
+ showCount = this.state.reposList.length;
+ }
+ if (STATUS !== ATTEMPTING && !this.state.paginationMode) {
+ maybeMessage = {`Showing ${showCount} of ${showTotal}`};
+ } else if (STATUS === ATTEMPTING) {
+ maybeMessage = (
+
+ Loading ...
+
+ );
+ }
+
+ let maybeFilter = (
+
+ );
+
+ // Do not show state option unless country is US or Canada
+ let stateArea;
+ if (includes(COUNTRIES_WITH_STATES, values.country)) {
+ const statesOrProvinces = values.country === UNITED_STATES ? states : provinces;
+ stateArea = (
+
+ You may { 'upload the license file, ' +
+ 'provided below, in '} Settings > Licenses.
+ Save and Restart UCP.
+
+
+
+
Licensing DTR
+
+ {'Before proceeding, you must set the domain name to the full ' +
+ 'host-name of your Trusted Registry server (this is under '}Settings
+ {' in Trusted Registry). Once you’ve saved and restarted the Trusted Registry, you may now '}
+ { 'upload the license file, ' +
+ 'provided below, in '} Settings > Licenses.
+ Save and Restart once more.
+
+ {'Once the engine is installed, '}
+ install Docker Trusted Registry
+ by running the docker/dtr
+ container below.
+ {' This command pulls and runs Docker Trusted Registry on a container.'}
+
+
+ {`NOTE: Your browser will warn you that this is an unsafe site, ` +
+ `with a self-signed, untrusted certificate. This is normal and ` +
+ `expected; please allow this connection temporarily.`}
+
+
+ { 'You’re almost ready to push and pull images! You need to ' }
+ secure your Trusted Registry
+ first. Navigate to Settings > Security{', and enter your ' +
+ 'data in the required fields. At this time, you may also want to ' +
+ 'configure additional settings such as ports, storage, authentication, and so forth.' }
+
+ );
+ }
+}
diff --git a/app/scripts/components/enterprise/EnterpriseTrialSuccess/StepsTab.jsx b/app/scripts/components/enterprise/EnterpriseTrialSuccess/StepsTab.jsx
new file mode 100644
index 0000000000..feae8301d3
--- /dev/null
+++ b/app/scripts/components/enterprise/EnterpriseTrialSuccess/StepsTab.jsx
@@ -0,0 +1,54 @@
+'use strict';
+
+import React, { PropTypes, Component } from 'react';
+import { Link } from 'react-router';
+import styles from '../EnterpriseTrialSuccess.css';
+import classnames from 'classnames';
+import FA from 'common/FontAwesome';
+const { number, string } = PropTypes;
+
+export default class StepsTab extends Component {
+ static propTypes = {
+ currentStep: number.isRequired,
+ namespace: string.isRequired,
+ step: number.isRequired,
+ title: string.isRequired
+ }
+
+ render() {
+ const { currentStep,
+ namespace,
+ step,
+ title } = this.props;
+ const classes = classnames({
+ [styles.tab]: true,
+ [styles.active]: currentStep === step,
+ [styles.success]: currentStep > step,
+ [styles.last]: step === 4
+ });
+ let icon;
+ //TODO: replace the with line below when FA can accept size=1x
+ //
+ if (currentStep > step) {
+ icon = (
+
+
+
+
+ );
+ } else {
+ icon = (
+
+
+ {step}
+
+ );
+ }
+ return (
+
+
+ { icon } { title }
+
+ );
+ }
+}
diff --git a/app/scripts/components/enterprise/EnterpriseTrialTerms.css b/app/scripts/components/enterprise/EnterpriseTrialTerms.css
new file mode 100644
index 0000000000..72fad5d928
--- /dev/null
+++ b/app/scripts/components/enterprise/EnterpriseTrialTerms.css
@@ -0,0 +1,7 @@
+.softwareAgreement {
+ padding-top: .2rem;
+}
+
+.termsPageWrapper {
+ padding-top: 1.25rem;
+}
\ No newline at end of file
diff --git a/app/scripts/components/enterprise/EnterpriseTrialTerms.jsx b/app/scripts/components/enterprise/EnterpriseTrialTerms.jsx
new file mode 100644
index 0000000000..cd4c8cc871
--- /dev/null
+++ b/app/scripts/components/enterprise/EnterpriseTrialTerms.jsx
@@ -0,0 +1,87 @@
+'use strict';
+
+import React, { Component } from 'react';
+import Card, { Block } from '@dux/element-card';
+import { PageHeader } from 'dux';
+import styles from './EnterpriseTrialTerms.css';
+
+export default class extends Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+
This Docker software evaluation agreement("agreement") is by and between Docker Inc., located at 144 Townsend St, San Francisco, CA 94107("Docker") and the individual or legal entity who has executed an order form(or other ordering or purchasing document) referencing this agreement or is using the applicable software made available by Docker("customer") and governs all use by customer of the Docker software referenced in such order form. by executing an order form, customer expressly accepts and agrees to the terms of this agreement.if you are an individual agreeing to the terms of this agreement on behalf of an entity, such as your employer, you represent that you have the legal authority to bind that entity and "customer" shall refer herein to such entity.if you do not have such authority, or if you do not agree with the terms of this agreement, you must not execute the order form and may not use the licensed software(each as defined below). this agreement does not provide a commercial license after the trial period.use after the trial period is subject to the parties entering into and executing a separate written agreement.
+
1. Definitions.
+
The following capitalized terms shall have the meanings set forth below:
+
1.1 Feedback
+
"Feedback" means any comments or other feedback Customer may provide to Docker concerning the functionality and performance of the Licensed Software, including identification of potential errors and improvements.
+
1.2 Instance
+
"Instance" means a single instance of Licensed Software, as applicable, installed on a physical or virtual computer or server.
+
1.3 Key
+
Key means the license key or similar control mechanism to help ensure compliance with the use and time limitations with respect to the Licensed Software.
+
1.4 Licensed Software
+
Licensed Software means the Docker software identified on an Order Form (other than Open Source Software) and licensed to Customer pursuant to the terms of this Agreement, e.g., the "Docker trusted registry" software or other licensed software from Docker that is identified on the Order Form (excluding any Open Source Software included therein).
+
1.5 Open Source Software
+
Open Source Software means Docker or third party software identified at
+ Docker.com/components-licenses, that is distributed or otherwise made available as "free software", "open source software" or under a similar licensing or distribution model.
+
1.6 Order Form
+
Order Form means an ordering document referencing this Agreement between Customer and Docker
+
1.7 Trial Period
+
"Trial Period" means 30 days
+
2. License.
+
2.1 Licensed Software.
+
Licensed Software. Subject to Customer's compliance with the terms and conditions of this Agreement, Docker hereby grants Customer a limited, non-exclusive, non-transferable, non-sub-licensable license during the Trial Period to install, copy and use the Licensed Software solely for Customer's internal evaluation purposes, in connection with the deployment of no more than one Instance.
+
2.2 Open Source Software.
+
If applicable, Open Source Software is distributed or made available under the terms of the open source license agreements referenced in the applicable distribution or the applicable help, notices, about or source files. Copyrights and other proprietary rights to the Open Source Software are held by the copyright holders identified in the applicable distribution or the applicable help, notices, about or source files.
+
2.3 License Keys.
+
Customer shall not destroy, disable or circumvent, or attempt to destroy, disable or circumvent in any way the Key and/or the use and time limitations set by the Key or the Licensed Software. Customer acknowledges and agrees that any attempt to exceed the use of the Licensed Software beyond the limits configured into the Key will automatically and immediately terminate the licenses granted under this Agreement.
+
3. Payment.
+
Subject to Customer's compliance with the terms and conditions of this Agreement, and solely during the Trial Period, the Licensed Software shall be provided to Customer free of charge.
+
4. Restricted Activities.
+
Customer shall not, and shall not encourage any third party to: (a) modify, adapt, alter, translate, or create derivative works of the Licensed Software; (b) reverse-engineer, decompile, disassemble, or attempt to derive the source code for the Licensed Software, in whole or in part, except to the extent that such activities are permitted under applicable law; (c) distribute, license, sublicense, lease, rent, loan, or otherwise transfer the Licensed Software to any third party; (d) remove, alter, or obscure in any way the proprietary rights notices (including copyright, patent, and trademark notices and symbols) of Docker or its suppliers contained on or within any copies of the Licensed Software; (e) use the Licensed Software for the purpose of creating a product or service competitive with the Licensed Software; (f) use the Licensed Software with any unsupported software or hardware (as described in the applicable documentation provided by Docker); (g) use the Licensed Software for any time-sharing, outsourcing, service bureau, hosting, application service provider or like purposes; (h) disclose the results of any benchmark tests on the Licensed Software without Docker's prior written consent; or (i) use the Licensed Software other than as described in the documentation provided therewith, or for any unlawful purpose.
+
5. Ownership of Licensed Software.
+
Docker and its licensors own and retain all right, title, and interest, including all intellectual property rights, in and to the Licensed Software, including any improvements, modifications, and enhancements to it. Except for the rights expressly granted in this Agreement, Customer shall acquire no other rights, express or implied, in or to the Licensed Software, and all rights not expressly provided to Customer hereunder are reserved by Docker and its licensors. All the copies of the Licensed Software provided or made available hereunder are licensed, not sold.
+
6. Term.
+
Unless otherwise terminated in accordance with this section, this Agreement will remain in effect until the expiration of the Trial Period. Either party may immediately terminate this Agreement and any Order Form incorporating the terms of this Agreement if the other party materially breaches this Agreement. Either party may terminate this agreement without cause upon 10 days' prior written notice. Unless otherwise agreed by the parties, upon the expiration or termination of the Trial Period all licenses granted herein will automatically terminate and Customer will discontinue all use of the applicable Licensed Software and will return to Docker any materials (including any copies of Licensed Software) provided by Docker to Customer. Sections 1, 2.2, 4, 5, and 7 through 14 shall survive any termination or expiration of this Agreement or any Order Form.
+
7.Feedback.
+
Customer may submit to Docker bug reports, comments, feedback or ideas about the Licensed Software, including without limitation about how to improve the Licensed Software. By submitting any Feedback, Customer hereby assigns to Docker all right, title, and interest in and to the Feedback, if any.
+
8. Confidentiality.
+
8.1 Definition.
+
"Confidential Information" means any information disclosed by one party ("Discloser") to the other ("Recipient"), directly or indirectly, in writing, orally or by inspection of tangible objects, which is designated as "Confidential," "Proprietary" or some similar designation, or learned by Recipient under circumstances in which such information would reasonably be understood to be confidential. Confidential Information may include information disclosed in confidence to Discloser by third parties. For the purposes of this Agreement, the Licensed Software, and the results of any performance, functional or other evaluation of the Licensed Software, shall be deemed Confidential Information of Docker.
+
8.2 Exceptions.
+
The confidentiality obligations in this Section 8 shall not apply with respect to any of the Discloser's Confidential information which Recipient can demonstrate: (a) was in the public domain at the time it was disclosed to Recipient or has become in the public domain through no act or omission of Recipient; (b) was known to Recipient, without restriction, at the time of disclosure as shown by the files of Recipient in existence at the time of disclosure; (c) was disclosed by Recipient with the prior written approval of Discloser; (d) was independently developed by Recipient without any use of Discloser's Confidential Information by employees or other agents of (or contractors hired by) Recipient who had no access to or did not rely on Discloser's Confidential Information; or (e) became known to Recipient, without restriction, from a source other than Discloser without breach of this Agreement by Recipient and otherwise not in violation of Discloser's rights.
+
8.3 Restrictions on Use and Disclosure.
+
Recipient agrees not to use Discloser's Confidential Information or disclose, distribute or disseminate Discloser's Confidential Information except in furtherance of the performance of its obligations or enforcement of its rights hereunder or as otherwise expressly agreed by Discloser in writing. Recipient agrees to restrict access to such Confidential Information to those employees or consultants of Recipient who need to know such Confidential Information for performing as contemplated hereunder and have agreed in writing to be bound by a confidentiality obligation no less protective than that contained in this Agreement. Recipient shall exercise the same degree of care to prevent unauthorized use or disclosure of Discloser's Confidential Information to others as it takes to preserve and safeguard its own information of like importance, but in no event less than reasonable care.
+
8.4 Compelled Disclosure.
+
If Recipient is compelled by a court or other competent authority or applicable law to disclose Confidential Information of Discloser, it shall give Discloser prompt written notice and shall provide Discloser with reasonable cooperation at Discloser's expense so that Discloser may take steps to oppose such disclosure or obtain a restraining order. Recipient shall not be in breach of its obligations in this Section 9 if it makes any legally compelled disclosure provided that Recipient meets the foregoing notice and cooperation requirements.
+
8.5 Injunctive Relief.
+
Recipient acknowledges that breach of the confidentiality obligations would cause irreparable harm to Discloser, the extent of which may be difficult to ascertain. Accordingly, Recipient agrees that Discloser is entitled to immediate injunctive relief in the event of breach of an obligation of confidentiality by Recipient, and that Discloser shall not be required to post a bond or show irreparable harm in order to obtain such injunctive relief.
+
8.6 Return of Confidential Information.
+
As between the parties, Confidential Information shall remain the property of the Discloser. At any time, upon Discloser's reasonable request, Recipient shall promptly (and in any event within 30 days) return to Discloser or destroy, at the election of the Discloser, any Confidential Information of the Discloser. In addition, within 30 days after termination of this Agreement, Recipient shall (i) promptly return all tangible materials containing such Confidential Information to Discloser, (ii) remove all Confidential Information (and any copies thereof) from any computer systems of the Recipient, its contractors and its distributors, and confirm in writing that all materials containing Confidential Information have been destroyed or returned to Discloser, as applicable, by Recipient. Recipient shall cause its affiliates, agents, contractors, and employees to strictly comply with the foregoing.
+
9. No Warranties.
+
CUSTOMER EXPRESSLY UNDERSTANDS AND AGREES THAT ALL USE OF THE LICENSED SOFTWARE IS AT CUSTOMER'S SOLE RISK AND THAT THE LICENSED SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE." DOCKER, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS MAKE NO EXPRESS WARRANTIES AND DISCLAIM ALL IMPLIED WARRANTIES REGARDING THE LICENSED SOFTWARE, INCLUDING IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT, TOGETHER WITH ANY AND ALL WARRANTIES ARISING FROM COURSE OF DEALING OR USAGE IN TRADE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM DOCKER OR ELSEWHERE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THIS AGREEMENT. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, DOCKER, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT: (A) CUSTOMER'S USE OF THE LICENSED SOFTWARE WILL MEET CUSTOMER'S REQUIREMENTS, OR (B) CUSTOMER'S USE OF THE LICENSED SOFTWARE WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR. NOTWITHSTANDING THE FOREGOING, NOTHING HEREIN SHALL EXCLUDE OR LIMIT DOCKER'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW. CUSTOMER UNDERSTANDS AND ACKNOWLEDGES THAT THE LICENSED SOFTWARE IS NOT DESIGNED, INTENDED OR WARRANTED FOR USE IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE CONTROLS, INCLUDING WITHOUT LIMITATION, OPERATION OF NUCLEAR FACILITIES, AIRCRAFT NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, AND LIFE SUPPORT OR WEAPONS SYSTEMS.
+
10. Indemnification by Customer.
+
Customer agrees to hold harmless and indemnify Docker and its subsidiaries, affiliates, officers, agents, employees, advertisers, licensors, suppliers or partners from and against any third party claim arising from or in any way related to Customer's breach of this Agreement, use of the Licensed Software, or violation of applicable laws, rules or regulations in connection with the Licensed Software, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, Docker will provide Customer with written notice of such claim, suit or action.
+
11. Limitation of Liability.
+
11.1 Exclusion of Damages
+
Exclusion of Damages. CUSTOMER EXPRESSLY UNDERSTANDS AND AGREES THAT DOCKER, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO CUSTOMER FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL CONSEQUENTIAL OR EXEMPLARY DAMAGES INCURRED BY CUSTOMER, HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, INCLUDING, BUT NOT LIMITED TO, ANY LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, OR OTHER INTANGIBLE LOSS. THE FOREGOING LIMITATIONS ON DOCKER'S LIABILITY SHALL APPLY WHETHER OR NOT DOCKER HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING. NOTWITHSTANDING THE FOREGOING, NOTHING HEREIN SHALL EXCLUDE OR LIMIT DOCKER'S LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW.
+
11.2 Liability Cap
+
THE TOTAL LIABILITY OF DOCKER ARISING OUT OF OR RELATED TO THIS AGREEMENT WILL NOT EXCEED USD $100.
+
12. Export Restrictions.
+
Customer understands that the Licensed Software may contain encryption technology and other software programs that may require an export license from the U.S. State Department and that export or re-export of the Licensed Software to certain entities (such as a foreign government and its subdivisions) and certain countries is prohibited. Customer acknowledges that it will comply with all applicable export and import control laws and regulations of the United States and the foreign jurisdiction in which the Licensed Software is used and, in particular, Customer will not export or re-export the Licensed Software without all required United States and foreign government licenses. Customer will defend, indemnify, and hold harmless Docker and its suppliers and licensors from and against any violation of such laws or regulations by Customer or any of its agents, officers, directors or employees.
+
13. Miscellaneous.
+
The Licensed Software and any other software covered under this Agreement are "commercial items" as that term is defined at 48 C.F.R. 2.101; consisting of "commercial computer software" and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212. Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4, all U.S. Government end users acquire the Licensed Software and any other software and documentation covered under this Agreement with only those rights set forth herein. This Agreement will be governed by the laws of the State of California without reference to conflict of law principles. Each party agrees to submit to the exclusive jurisdiction of the courts located within the county of San Francisco, California to resolve any legal matter arising from this Agreement. Neither party may assign any of its rights or obligations under this Agreement, whether by operation of law or otherwise, without the prior written consent of the other party (not to be unreasonably withheld). Notwithstanding the foregoing, Docker may assign the entirety of its rights and obligations under this Agreement, without consent of the Customer, to its affiliate or in connection with a merger, acquisition, corporate reorganization, or sale of all or substantially all of its assets. The application of the UN Convention of International Sale of Goods to this Agreement is disclaimed in its entirety. Together with any Order Forms, this is the entire agreement between the parties relating to the subject matter hereof. This Agreement (including applicable Order Forms) shall control over any additional or different terms of any correspondence, order, confirmation, invoice or similar document, even if accepted in writing by both parties, and waivers and amendments of any provision of this Agreement shall be effective only if made by non-preprinted agreements indicating specifically what sections of this Agreement are affected, signed by both parties and clearly understood by both parties to be an amendment or waiver. The failure of either party to enforce its rights under this Agreement at any time for any period shall not be construed as a waiver of such rights. If any provision of this Agreement is held invalid or unenforceable, the remainder of this Agreement will continue in full force and effect and the invalid or unenforceable provision shall be reformed to the extent necessary to make it valid and enforceable.
+
+ Max of 1 Docker Trusted Registry for Server and 10 Engines
+
+
+
+
+
+
+ );
+ }
+ }
+});
+
+export default connectToStores(EnterprisePaid,
+ [
+ EnterprisePartnerTrackingStore
+ ],
+ function({ getStore }, props) {
+ return getStore(EnterprisePartnerTrackingStore).getState();
+ });
diff --git a/app/scripts/components/enterprise/plansAndSubscriptions.js b/app/scripts/components/enterprise/plansAndSubscriptions.js
new file mode 100644
index 0000000000..eaa52d67d9
--- /dev/null
+++ b/app/scripts/components/enterprise/plansAndSubscriptions.js
@@ -0,0 +1,103 @@
+'use strict';
+
+export default {
+ server: {
+ type: 'Docker Trusted Registry',
+ name: 'Server Starter Edition includes',
+ includes: [
+ '1 instance of Docker Trusted Registry',
+ '10 Docker Engines with commercial support for the servers hosting your application',
+ 'Email support service levels for your Docker software'
+ ],
+ redirect_value: 'eval',
+ notes: 'License keys and commercially supported Docker Engine software are distributed and managed within your Docker Hub account.'
+ },
+ trial: {
+ type: 'a trial of Docker Datacenter',
+ name: 'Docker Datacenter Trial includes access to',
+ includes: [
+ 'Trusted Registry',
+ 'Universal Control Plane',
+ 'Commercial Support for Docker Engine'
+ ],
+ redirect_value: 'eval',
+ notes: 'License keys and commercially supported Docker Engine software are distributed and managed within your Docker Hub account.'
+ },
+ cloud: {
+ type: 'Docker Cloud Subscription',
+ name: 'Cloud Starter Edition includes',
+ includes: [
+ '20 Private Repos',
+ '10 Docker Engines with commercial support for the servers hosting your application',
+ 'Email support service levels for your Docker software'
+ ],
+ redirect_value: 'cloud_starter',
+ notes: 'License keys and commercially supported Docker Engine software are distributed and managed within your Docker Hub account.'
+ },
+ micro: {
+ type: 'our Private Repo Plans',
+ name: 'Micro plan includes',
+ includes: [
+ '5 Private Repos',
+ '5 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'micro',
+ notes: null
+ },
+ small: {
+ type: 'our Private Repo Plans',
+ name: 'Small plan includes',
+ includes: [
+ '10 Private Repos',
+ '10 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'small',
+ notes: null
+ },
+ medium: {
+ type: 'our Private Repo Plans',
+ name: 'Medium plan includes',
+ includes: [
+ '20 Private Repos',
+ '20 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'medium',
+ notes: null
+ },
+ large: {
+ type: 'our Private Repo Plans',
+ name: 'Large plan includes',
+ includes: [
+ '50 Private Repos',
+ '50 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'large',
+ notes: null
+ },
+ xlarge: {
+ type: 'our Private Repo Plans',
+ name: 'XLarge plan includes',
+ includes: [
+ '100 Private Repos',
+ '100 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'xlarge',
+ notes: null
+ },
+ xxlarge: {
+ type: 'our Private Repo Plans',
+ name: 'XX-Large plan includes',
+ includes: [
+ '250 Private Repos',
+ '250 Parallel Builds',
+ 'Community Hub Support'
+ ],
+ redirect_value: 'xxlarge',
+ notes: null
+ }
+};
diff --git a/app/scripts/components/eusa.js b/app/scripts/components/eusa.js
new file mode 100644
index 0000000000..cf474ae4d0
--- /dev/null
+++ b/app/scripts/components/eusa.js
@@ -0,0 +1,455 @@
+'use strict';
+
+module.exports = {
+ md: `**DOCKER SOFTWARE END USER SUBSCRIPTION AGREEMENT**
+===================================================
+
+THIS DOCKER SOFTWARE END USER SUBSCRIPTION AGREEMENT ("AGREEMENT") IS BY
+AND BETWEEN DOCKER INC., LOCATED AT 144 TOWNSEND ST, SAN FRANCISCO, CA
+94107 ("DOCKER") AND THE INDIVIDUAL OR LEGAL ENTITY WHO HAS EXECUTED AN
+ORDER FORM (OR OTHER ORDERING OR PURCHASING DOCUMENT) REFERENCING THIS
+AGREEMENT OR IS USING THE APPLICABLE SOFTWARE MADE AVAILABLE BY DOCKER
+("CUSTOMER") AND GOVERNS ALL USE BY CUSTOMER OF THE DOCKER SOFTWARE
+REFERENCED IN SUCH ORDER FORM.
+
+BY EXECUTING AN ORDER FORM, CUSTOMER EXPRESSLY ACCEPTS AND AGREES TO THE
+TERMS OF THIS AGREEMENT. IF YOU ARE AN INDIVIDUAL AGREEING TO THE TERMS
+OF THIS AGREEMENT ON BEHALF OF AN ENTITY, SUCH AS YOUR EMPLOYER, YOU
+REPRESENT THAT YOU HAVE THE LEGAL AUTHORITY TO BIND THAT ENTITY AND
+"CUSTOMER" SHALL REFER HEREIN TO SUCH ENTITY. IF YOU DO NOT HAVE SUCH
+AUTHORITY, OR IF YOU DO NOT AGREE WITH THE TERMS OF THIS AGREEMENT, YOU
+MUST NOT EXECUTE THE ORDER FORM AND MAY NOT USE THE LICENSED SOFTWARE OR
+THE SUBSCRIPTION SERVICES (EACH AS DEFINED BELOW).
+
+### 1. Definitions
+
+The following capitalized terms shall have the meanings set forth below:
+"Docker Authorized Business Partner" shall have the meaning ascribed to
+that term in Section 3.3.
+
+1.1 **"Docker Authorized Business Partner"** shall have the meaning
+ascribed to that term in Section 3.3.
+
+1.2 **"Feedback"** means any comments or other feedback Customer may
+provide to Docker concerning the functionality and performance of the
+Supported Software, including identification of potential errors and
+improvements.
+
+1.3 **"Instance"** means a single instance of Licensed Software or
+Supported Software, as applicable, installed on a physical or virtual
+computer or server.
+
+1.4 **"Key"** means the license key or similar control mechanism to help
+ensure compliance with the use and time limitations with respect to the
+Licensed Software.
+
+1.5 **"Licensed Software"** means the Docker software identified on an
+Order Form (other than Open Source Software) and licensed to Customer
+pursuant to the terms of this Agreement, e.g., the "Docker Trusted
+Registry" software or other licensed software from Docker that is
+identified on the Order Form (excluding any Open Source Software
+included therein).
+
+1.6 **"Open Source Software"** means Docker or third party software
+identified at [placeholder for webpage or other documentation with open
+source listing], that is distributed or otherwise made available as
+"free software", "open source software" or under a similar licensing or
+distribution model.
+
+1.7 **"Order Form"** means an ordering document referencing this
+Agreement between Customer and Docker, or between Customer and a Docker
+Authorized Business Partner.
+
+1.8 **"Subscription Fee"** means the fee for Subscription Services
+purchased by the Customer. The amount of the Subscription Fee is based
+on the number of Instances and the level (e.g., 24X7 or Defined Business
+Hours) of Subscription Services specified in the Order Form
+"Subscription Term" means the applicable initial and/or renewal
+subscription term as set forth in the applicable Order Form.
+
+1.9 **"Subscription Term"** means the applicable initial and/or renewal
+subscription term as set forth in the applicable Order Form.
+
+1.10 **"Supported Software"** means the Docker or third party software
+identified on the Order Form as software for which Docker or its
+authorized resellers agree to provide Subscription Services to Customer.
+For purposes of clarity, Supported Software may include Licensed
+Software and/or identified versions of Open Source Software with respect
+to which Docker agrees to provide updates, patches and hotfixes to the
+customer.
+
+1.11 **"Subscription Services"** means standard support and maintenance
+services and software updates provided by Docker for the Supported
+Software, as set forth at:
+[*https://www.docker.com/support/*](https://www.docker.com/support/).
+
+2\. License
+
+2.1 **Licensed Software.** Subject to Customer's timely payment of the
+Subscription Fee and compliance with the terms and conditions of this
+Agreement, Docker hereby grants Customer a limited, non-exclusive,
+non-transferable, non-sub-licensable license during the applicable
+Subscription Term to install, copy and use the Licensed Software for
+Customer's internal business purposes, in connection with the deployment
+of no more than the number of Instances as are set forth in the Order
+Form.
+
+2.2 **Open Source Software.** If applicable, Open Source Software is
+distributed or made available under the terms of the open source license
+agreements referenced in the applicable distribution or the applicable
+help, notices, about or source files. Copyrights and other proprietary
+rights to the Open Source Software are held by the copyright holders
+identified in the applicable distribution or the applicable help,
+notices, about or source files.
+
+2.3 **License Keys.** Customer shall not destroy, disable or circumvent,
+or attempt to destroy, disable or circumvent in any way the Key and/or
+the use and time limitations set by the Key or the Licensed Software.
+Customer acknowledges and agrees that any attempt to exceed the use of
+the Licensed Software beyond the limits configured into the Key will
+automatically and immediately terminate the licenses granted under this
+Agreement.
+
+### 3. Subscription
+
+3.1 **Subscription Services.** Subject to Customer's timely payment of
+the Subscription Fee and compliance with the terms and conditions of
+this Agreement, Docker shall provide to Customer the Subscription
+Services during the Subscription Term. Customer must purchase
+Subscription Services corresponding to the number of Instances specified
+in the Order Form. Customer may purchase different levels of
+Subscription Services with respect to each such Instance; provided,
+however, that Customer may not use Subscription Services with a higher
+support level in connection with an Instance for which Customer had
+purchased Subscription Services with a lower support level. In addition,
+the customer may not use Instances of Open Source Software that has not
+been identified on an Order Form, on computers and or servers that are
+part of the environment in which Subscription Services are provided.
+Unless renewed, the Subscription Services will expire at the end of the
+applicable Subscription Term. This means that while the Customer is free
+to use the Open Source Software after the expiration of the applicable
+Subscription Term, Docker will not provide the Subscription Services
+after the end of the applicable Subscription Term.
+
+3.2 **Subscription Fee.** Payment of the Subscription Fee shall be made
+by Customer as set forth in the applicable Order Form.
+
+3.3 **Support from Docker's Business Partners.** In some cases,
+Customers may also receive support services,as part of the purchased
+Subscription Services, from a Docker authorized business partner (each,
+a "Docker Authorized Business Partner"). Notwithstanding anything to the
+contrary in Section 3.1, if Customer purchases support services from a
+Docker Authorized Business Partner, Docker shall have no obligation to
+provide any support services to the Customer and Customer should work
+with that Docker Authorized Business Partner to obtain all support
+services for the Supported Software.
+
+### 4. Restricted Activities
+
+Customer shall not, and shall not encourage any third party to: (a)
+modify, adapt, alter, translate, or create derivative works of the
+Licensed Software; (b) reverse-engineer, decompile, disassemble, or
+attempt to derive the source code for the Licensed Software, in whole or
+in part, except to the extent that such activities are permitted under
+applicable law; (c) distribute, license, sublicense, lease, rent, loan,
+or otherwise transfer the Licensed Software to any third party; (d)
+remove, alter, or obscure in any way the proprietary rights notices
+(including copyright, patent, and trademark notices and symbols) of
+Docker or its suppliers contained on or within any copies of the
+Licensed Software; (e) use the Licensed Software for the purpose of
+creating a product or service competitive with the Licensed Software;
+(f) use the Licensed Software with any unsupported software or hardware
+(as described in the applicable documentation provided by Docker); (g)
+use the Licensed Software for any time-sharing, outsourcing, service
+bureau, hosting, application service provider or like purposes; (h)
+disclose the results of any benchmark tests on the Licensed Software
+without Docker's prior written consent; or (i) use the Licensed Software
+other than as described in the documentation provided therewith, or for
+any unlawful purpose.
+
+### 5. Ownership of Licensed Software
+
+Docker and its licensors own and retain all right, title, and interest,
+including all intellectual property rights, in and to the Licensed
+Software, including any improvements, modifications, and enhancements to
+it. Except for the rights expressly granted in this Agreement, Customer
+shall acquire no other rights, express or implied, in or to the Licensed
+Software, and all rights not expressly provided to Customer hereunder
+are reserved by Docker and its licensors. All the copies of the Licensed
+Software provided or made available hereunder are licensed, not sold.
+
+### 6. Records and Audit
+
+Customer shall establish and maintain complete and accurate records
+related to the location, access and use of the Supported Software by
+Customer, its employees or its agents, and any such other information as
+reasonably necessary for Docker to verify compliance with the terms of
+this Agreement. Such records shall be kept for at least 3 years
+following the end of the quarter to which they pertain. Upon prior
+notice, Docker or its representative may inspect such records to confirm
+Customer's compliance with the terms of this Agreement. Prompt
+adjustments shall be made by Customer as directed by Docker to
+compensate for any errors or breach discovered by such audit, such as
+underpayment of the Subscription Fee, with the applicable late payment
+interest. Additionally, if Customer has underpaid Docker or its
+authorized reseller by more than 5% of the total amount owed hereunder,
+the cost of the audit shall be borne by Customer.
+
+### 7. Term
+
+Unless otherwise terminated in accordance with this section, this
+Agreement will remain in effect until all Subscription Services granted
+under this Agreement have expired. Either party may terminate this
+Agreement and any Order Form incorporating the terms of this Agreement
+(if Docker is a party to such Order Form) if the other party materially
+breaches this Agreement and fails to cure such breach within 30 days of
+receiving written notice thereof. Unless otherwise agreed by the
+parties, upon the expiration or termination of this Agreement or any
+Order Form all Subscription Services granted herein or therein will
+automatically terminate and Customer will discontinue all use of the
+applicable Licensed Software and Supported Software and will return to
+Docker any materials (including any copies of Licensed Software)
+provided by Docker to Customer. Sections 1, 2.4, 3, 5, and 7 through 14
+shall survive any termination or expiration of this Agreement or any
+Order Form.
+
+### 8. Feedback.
+
+Customer may submit to Docker bug reports, comments, feedback or ideas
+about the Supported Software, including without limitation about how to
+improve the Supported Software. By submitting any Feedback, Customer
+hereby assigns to Docker all right, title, and interest in and to the
+Feedback, if any.
+
+### 9. Confidentiality
+
+9.1 **Definition.** "Confidential Information" means any information
+disclosed by one party ("Discloser") to the other ("Recipient"),
+directly or indirectly, in writing, orally or by inspection of tangible
+objects, which is designated as "Confidential," "Proprietary" or some
+similar designation, or learned by Recipient under circumstances in
+which such information would reasonably be understood to be
+confidential. Confidential Information may include information disclosed
+in confidence to Discloser by third parties. For the purposes of this
+Agreement, the Licensed Software, and the results of any performance,
+functional or other evaluation of the Licensed Software, shall be deemed
+Confidential Information of Docker.
+
+9.2 **Exceptions.** The confidentiality obligations in this Section 8
+shall not apply with respect to any of the Discloser's Confidential
+information which Recipient can demonstrate: (a) was in the public
+domain at the time it was disclosed to Recipient or has become in the
+public domain through no act or omission of Recipient; (b) was known to
+Recipient, without restriction, at the time of disclosure as shown by
+the files of Recipient in existence at the time of disclosure; (c) was
+disclosed by Recipient with the prior written approval of Discloser; (d)
+was independently developed by Recipient without any use of Discloser's
+Confidential Information by employees or other agents of (or contractors
+hired by) Recipient who had no access to or did not rely on Discloser's
+Confidential Information; or (e) became known to Recipient, without
+restriction, from a source other than Discloser without breach of this
+Agreement by Recipient and otherwise not in violation of Discloser's
+rights.
+
+9.3 **Restrictions on Use and Disclosure.** Recipient agrees not to use
+Discloser's Confidential Information or disclose, distribute or
+disseminate Discloser's Confidential Information except in furtherance
+of the performance of its obligations or enforcement of its rights
+hereunder or as otherwise expressly agreed by Discloser in writing.
+Recipient agrees to restrict access to such Confidential Information to
+those employees or consultants of Recipient who need to know such
+Confidential Information for performing as contemplated hereunder and
+have agreed in writing to be bound by a confidentiality obligation no
+less protective than that contained in this Agreement. Recipient shall
+exercise the same degree of care to prevent unauthorized use or
+disclosure of Discloser's Confidential Information to others as it takes
+to preserve and safeguard its own information of like importance, but in
+no event less than reasonable care.
+
+9.4 **Compelled Disclosure.** If Recipient is compelled by a court or
+other competent authority or applicable law to disclose Confidential
+Information of Discloser, it shall give Discloser prompt written notice
+and shall provide Discloser with reasonable cooperation at Discloser's
+expense so that Discloser may take steps to oppose such disclosure or
+obtain a restraining order. Recipient shall not be in breach of its
+obligations in this Section 9 if it makes any legally compelled
+disclosure provided that Recipient meets the foregoing notice and
+cooperation requirements.
+
+9.5 **Injunctive Relief.** Recipient acknowledges that breach of the
+confidentiality obligations would cause irreparable harm to Discloser,
+the extent of which may be difficult to ascertain. Accordingly,
+Recipient agrees that Discloser is entitled to immediate injunctive
+relief in the event of breach of an obligation of confidentiality by
+Recipient, and that Discloser shall not be required to post a bond or
+show irreparable harm in order to obtain such injunctive relief.
+
+9.6 **Return of Confidential Information.** As between the parties,
+Confidential Information shall remain the property of the Discloser. At
+any time, upon Discloser’s reasonable request, Recipient shall promptly
+(and in any event within 30 days) return to Discloser or destroy, at the
+election of the Discloser, any Confidential Information of the
+Discloser. In addition, within 30 days after termination of this
+Agreement, Recipient shall (i) promptly return all tangible materials
+containing such Confidential Information to Discloser, (ii) remove all
+Confidential Information (and any copies thereof) from any computer
+systems of the Recipient, its contractors and its distributors, and
+confirm in writing that all materials containing Confidential
+Information have been destroyed or returned to Discloser, as applicable,
+by Recipient. Recipient shall cause its affiliates, agents, contractors,
+and employees to strictly comply with the foregoing.
+
+### 10. No Warranties
+
+CUSTOMER EXPRESSLY UNDERSTAND AND AGREE THAT ALL USE OF THE SUPPORTED
+SOFTWARE IS AT CUSTOMER'S SOLE RISK AND THAT THE SUPPORTED SOFTWARE AND
+SUPPORT SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE." DOCKER, ITS
+SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS MAKE NO EXPRESS
+WARRANTIES AND DISCLAIM ALL IMPLIED WARRANTIES REGARDING THE SUPPORTED
+SOFTWARE OR SUPPORT SERVICES, INCLUDING IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT,
+TOGETHER WITH ANY AND ALL WARRANTIES ARISING FROM COURSE OF DEALING OR
+USAGE IN TRADE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN,
+OBTAINED FROM DOCKER OR ELSEWHERE SHALL CREATE ANY WARRANTY NOT
+EXPRESSLY STATED IN THIS AGREEMENT. WITHOUT LIMITING THE GENERALITY OF
+THE FOREGOING, DOCKER, ITS SUBSIDIARIES AND AFFILIATES, AND ITS
+LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT: (A) CUSTOMER’S USE OF
+THE SUPPORTED SOFTWARE OR SUPPORT SERVICES WILL MEET CUSTOMER’S
+REQUIREMENTS, OR (B) CUSTOMER’S USE OF THE SUPPORTED SOFTWARE OR SUPPORT
+SERVICES WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR.
+NOTWITHSTANDING THE FOREGOING, NOTHING HEREIN SHALL EXCLUDE OR LIMIT
+DOCKER'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY
+EXCLUDED OR LIMITED BY APPLICABLE LAW. CUSTOMER UNDERSTANDS AND
+ACKNOWLEDGES THAT THE SUPPORTED SOFTWARE IS NOT DESIGNED, INTENDED OR
+WARRANTED FOR USE IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
+CONTROLS, INCLUDING WITHOUT LIMITATION, OPERATION OF NUCLEAR FACILITIES,
+AIRCRAFT NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, AND
+LIFE SUPPORT OR WEAPONS SYSTEMS.
+
+### 11. Indemnification
+
+11.1 **By Docker.** Docker shall defend at its own expense any legal
+action brought against Customer to the extent that it is based on a
+claim or allegation that the Licensed Software infringes a U.S. patent
+or copyright of a third party, and Docker will pay any costs and damages
+awarded against Customer in any such action, or agreed to under a
+settlement signed by Docker, that are attributable to any such claim but
+shall not be responsible for any compromise made or expense incurred
+without Docker’s consent. Such defense and payments are subject to the
+conditions that (a) Customer gives Docker prompt written notice of such
+claim, (b) tenders to Docker sole control of the defense and settlement
+of the claim, and (c) reasonably cooperates with Docker when requested
+in connection with the defense and settlement of the claim. Docker will
+have no liability to so defend and pay for any infringement claim to the
+extent it (i) is based on modification of the Licensed Software other
+than by Docker, with or without authorization; (ii) results from failure
+of Customer to use an updated version of the Licensed Software; or (iii)
+is based on the combination or use of the Licensed Software with any
+software (including, without limitation, Open Source Software), program
+or device not provided by Docker if such infringement would not have
+arisen but for such use or combination; or (iv) results from use of the
+Licensed Software by Customer after the license was terminated.
+
+11.2 **Limitation of IP Damages.** Should any Licensed Software, or the
+operation thereof, become or in Docker's opinion be likely to become,
+the subject of such claim described in Section 11.1, Docker may, at
+Docker's option and expense, procure for Customer the right to continue
+using the Licensed Software, replace or modify the Licensed Software so
+that it becomes non-infringing, or terminate the license granted
+hereunder for such Licensed Software. THIS SECTION 11 STATES DOCKER'S
+SOLE AND EXCLUSIVE LIABILITY, AND CUSTOMER'S SOLE AND EXCLUSIVE REMEDY,
+WITH RESPECT TO INFRINGEMENT OR MISAPPROPRIATION OF INTELLECTUAL
+PROPERTY RIGHTS OF ANY KIND.
+
+11.3 **By Customer.** Customer agrees to hold harmless and indemnify
+Docker and its subsidiaries, affiliates, officers, agents, employees,
+advertisers, licensors, suppliers or partners from and against any third
+party claim arising from or in any way related to Customer’s breach of
+this Agreement, use of the Supported Software, or violation of
+applicable laws, rules or regulations in connection with the Supported
+Software, including any liability or expense arising from all claims,
+losses, damages (actual and consequential), suits, judgments, litigation
+costs and attorneys' fees, of every kind and nature. In such a case,
+Docker will provide Customer with written notice of such claim, suit or
+action.
+
+### 12. Limitation of Liability.
+
+12.1 **Exclusion of Damages.** CUSTOMER EXPRESSLY UNDERSTANDS AND AGREES
+THAT DOCKER, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL
+NOT BE LIABLE TO CUSTOMER FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL
+CONSEQUENTIAL OR EXEMPLARY DAMAGES INCURRED BY CUSTOMER, HOWEVER CAUSED
+AND UNDER ANY THEORY OF LIABILITY, INCLUDING, BUT NOT LIMITED TO, ANY
+LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF
+GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, OR OTHER INTANGIBLE LOSS.
+THE FOREGOING LIMITATIONS ON DOCKER'S LIABILITY SHALL APPLY WHETHER OR
+NOT DOCKER HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE
+POSSIBILITY OF ANY SUCH LOSSES ARISING. NOTWITHSTANDING THE FOREGOING,
+NOTHING HEREIN SHALL EXCLUDE OR LIMIT DOCKER'S LIABILITY FOR LOSSES
+WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW.
+
+12.2 **Liability Cap.** EXCEPT WITH RESPECT TO EITHER PARTY’S
+OBLIGATIONS OF INDEMNIFICATION, THE TOTAL LIABILITY OF DOCKER ARISING
+OUT OF OR RELATED TO THIS AGREEMENT WILL NOT EXCEED THE GREATER OF USD
+\$100 OR THE TOTAL AMOUNTS PAID BY CUSTOMER FOR THE RELEVANT SUPPORTED
+SOFTWARE UNDER THE APPLICABLE ORDER FORM, IN THE TWELVE (12) MONTH
+PERIOD IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE LIABILITY.
+
+### 13. Export Restrictions.
+
+Customer understands that the Supported Software may contain encryption
+technology and other software programs that may require an export
+license from the U.S. State Department and that export or re-export of
+the Supported Software to certain entities (such as a foreign government
+and its subdivisions) and certain countries is prohibited. Customer
+acknowledges that it will comply with all applicable export and import
+control laws and regulations of the United States and the foreign
+jurisdiction in which the Supported Software is used and, in particular,
+Customer will not export or re-export the Supported Software without all
+required United States and foreign government licenses. Customer will
+defend, indemnify, and hold harmless Docker and its suppliers and
+licensors from and against any violation of such laws or regulations by
+Customer or any of its agents, officers, directors or employees.
+
+### 14. Miscellaneous
+
+The Supported Software and any other software covered under this
+Agreement are "commercial items" as that term is defined at 48 C.F.R.
+2.101; consisting of "commercial computer software" and "commercial
+computer software documentation" as such terms are used in 48 C.F.R.
+12.212. Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1
+through 227.7202-4, all U.S. Government end users acquire the Supported
+Software and any other software and documentation covered under this
+Agreement with only those rights set forth herein. This Agreement will
+be governed by the laws of the State of California without reference to
+conflict of law principles. Each party agrees to submit to the exclusive
+jurisdiction of the courts located within the county of San Francisco,
+California to resolve any legal matter arising from this Agreement.
+Neither party may assign any of its rights or obligations under this
+Agreement, whether by operation of law or otherwise, without the prior
+written consent of the other party (not to be unreasonably withheld).
+Notwithstanding the foregoing, Docker may assign the entirety of its
+rights and obligations under this Agreement, without consent of the
+Customer, to its affiliate or in connection with a merger, acquisition,
+corporate reorganization, or sale of all or substantially all of its
+assets. The application of the UN Convention of International Sale of
+Goods to this Agreement is disclaimed in its entirety. Together with any
+Order Forms, this is the entire agreement between the parties relating
+to the subject matter hereof. This Agreement (including applicable Order
+Forms) shall control over any additional or different terms of any
+correspondence, order, confirmation, invoice or similar document, even
+if accepted in writing by both parties, and waivers and amendments of
+any provision of this Agreement shall be effective only if made by
+non-preprinted agreements indicating specifically what sections of this
+Agreement are affected, signed by both parties and clearly understood by
+both parties to be an amendment or waiver. The failure of either party
+to enforce its rights under this Agreement at any time for any period
+shall not be construed as a waiver of such rights. If any provision of
+this Agreement is held invalid or unenforceable, the remainder of this
+Agreement will continue in full force and effect and the invalid or
+unenforceable provision shall be reformed to the extent necessary to
+make it valid and enforceable. When a new Subscription Term begins the
+most current version of this Agreement made available by Docker shall be
+applicable to Customer's use of the Software.`
+};
diff --git a/app/scripts/components/filter/FilterBar.css b/app/scripts/components/filter/FilterBar.css
new file mode 100644
index 0000000000..1caf8b5154
--- /dev/null
+++ b/app/scripts/components/filter/FilterBar.css
@@ -0,0 +1,19 @@
+
+.filterInput {
+ input[type='text'] {
+ background-color: white;
+ border-right: 0;
+ &:focus {
+ border-color: #ccc;
+ }
+ }
+}
+
+.iconStyle {
+ border-left: 0;
+ background: white !important;
+ line-height: 2.5;
+ &:hover {
+ cursor: pointer;
+ }
+}
\ No newline at end of file
diff --git a/app/scripts/components/filter/FilterBar.jsx b/app/scripts/components/filter/FilterBar.jsx
new file mode 100644
index 0000000000..6058646589
--- /dev/null
+++ b/app/scripts/components/filter/FilterBar.jsx
@@ -0,0 +1,66 @@
+'use strict';
+import React, { createClass, PropTypes } from 'react';
+import FA from 'common/FontAwesome';
+import styles from './FilterBar.css';
+const { bool, func, string } = PropTypes;
+const debug = require('debug')('COMPONENT:FilterBar');
+
+const FilterBar = createClass({
+ //Optional placeholder string
+ //Filter function to invoke when the query is submitted
+ propTypes: {
+ placeholder: string,
+ onFilter: func.isRequired,
+ onClick: func
+ },
+ getDefaultProps() {
+ return {
+ placeholder: 'Type to filter'
+ };
+ },
+ getInitialState() {
+ return {
+ query: ''
+ };
+ },
+ _clearQuery: function(event) {
+ this.setState({
+ query: ''
+ });
+ this.props.onFilter('');
+ },
+ _handleQueryChange: function(event) {
+ event.preventDefault();
+ this.props.onFilter(event.target.value);
+ this.setState({query: event.target.value});
+ },
+ _onSubmit: function(event) {
+ event.preventDefault();
+ },
+ render: function() {
+ const { onClick, placeholder } = this.props;
+ const { query } = this.state;
+ const maybeCancel = query ? '╳' : '';
+
+ return (
+
+ );
+ }
+
+ componentDidMount() {
+ const teamExists = find(this.props.teams, (team) => {
+ return team.name === this.props.location.query.team;
+ });
+ if (!this.props.location.query.team) {
+ //If there are teams, on load navigate to the first team and show members
+ if (this.props.teams && this.props.teams.length > 0) {
+ this.props.history.pushState(null, `/u/${this.props.currentUserContext}/dashboard/teams/`, {team: this.props.teams[0].name});
+ }
+ } else if (!teamExists) {
+ //If the team query isn't one of the orgs teams navigate to first in list
+ this.props.history.pushState(null, `/u/${this.props.currentUserContext}/dashboard/teams/`, {team: this.props.teams[0].name});
+ }
+ }
+
+ render() {
+ var addOrUpdateTeamForm;
+ if (this.state.addingTeam && !this.props.teamReadOnly) {
+ addOrUpdateTeamForm = (
+
+
+
+
+ {`The build rules below specify how to build your source into Docker images.
+ The name can be a string or a regex. The Docker Tag name may contain variables.
+ We currently support {sourceref}, which refers to the source branch/tag name.`}
+ {!this.state.isContextualHelpOpen ? ( Show more ) : ( Show less )}
+
+
+ Trigger endpoints are activated. Use the trigger token or URL below in
+ your requests.
+
+ { showExamples ? ' Hide examples.' : ' Show examples.' }
+
+
+ );
+
+ let triggerToken = null;
+ if (this.props.triggerStatus.token) {
+ triggerToken = (
+
+
Trigger Token
+
+
+
+
+
+ {regen}
+
+
+
+ );
+ }
+ let triggerUrl = null;
+ if (this.props.triggerStatus.trigger_url) {
+ triggerUrl = (
+
+
Trigger URL
+
+
+
+
+
+
+ );
+ }
+ return (
+
+
Note: Build requests are throttled so that they don't
+ overload the system. If there is already a build request
+ pending, the request will be ignored.
+ {examplesToggle}
+ {button}
+
+
+ {triggerToken}
+ {triggerUrl}
+ {examples}
+
+
+
+ );
+ }
+});
+
+// Currently logs will be static
+var TriggerLogs = createClass({
+ PropTypes: {
+ triggerLogs: PropTypes.array.isRequired
+ },
+ _makeLogs() {
+ if (this.props.triggerLogs.length === 0) {
+ return (
+
+
+ No logs to show
+
+
+ );
+ } else {
+ return (
+
+ If your automated build is linked to private repository on github or bitbucket,
+ we need a way to have access to the repository. We do this with deploy keys.
+ We try to do this step automatically for you, but sometimes we don't have access to do it.
+ When this happens you need to add it yourself.
+
+ A webhook is an HTTP call-back triggered by a specific event.
+ You can create a single webhook to start and connect multiple
+ webhooks to further build out your workflow.
+
+ );
+ }
+
+}
diff --git a/app/scripts/components/repo/repo_details/ScannedTag.css b/app/scripts/components/repo/repo_details/ScannedTag.css
new file mode 100644
index 0000000000..d577917f10
--- /dev/null
+++ b/app/scripts/components/repo/repo_details/ScannedTag.css
@@ -0,0 +1,8 @@
+.scanTitle {
+ font-size: 1.2rem;
+}
+
+/* add extra padding so that borders work */
+.wrapper {
+ padding: 0.5rem;
+}
diff --git a/app/scripts/components/repo/repo_details/ScannedTag.jsx b/app/scripts/components/repo/repo_details/ScannedTag.jsx
new file mode 100644
index 0000000000..08accb3303
--- /dev/null
+++ b/app/scripts/components/repo/repo_details/ScannedTag.jsx
@@ -0,0 +1,124 @@
+'use strict';
+
+import React, { PropTypes, Component } from 'react';
+import Card, { Block } from '@dux/element-card';
+import { connect } from 'react-redux';
+import { createSelector, createStructuredSelector } from 'reselect';
+import {
+ getComponents,
+ getComponentsBySeverity,
+ getLayers,
+ getScan,
+ getVulnerabilities,
+ getVulnerabilitiesByLayer
+} from './scannedTag/selectors';
+import { ERROR } from 'reduxConsts';
+import forEach from 'lodash/collection/forEach';
+import ScanHeader from './scannedTag/ScanHeader.jsx';
+import Layer from './scannedTag/Layer.jsx';
+import styles from './ScannedTag.css';
+import { Map } from 'immutable';
+import { getStatus } from 'selectors/status';
+
+const { object, array, instanceOf, shape } = PropTypes;
+
+// TODO: conversion to records
+// Right now we're storing only *one* scan in the reducer, therefore all entities
+// can be merged in to the scan as we know they all belong to this scan.
+//
+// This produces a denormalized, nested scan with all entities as the child of the scan.
+const getFullScan = createSelector(
+ [getScan, getLayers, getComponents, getVulnerabilities],
+ (scan, layers, components, vulns) => ({
+ ...scan,
+ layers,
+ components,
+ vulnerabilities: vulns
+ })
+);
+
+let mapState = createStructuredSelector({
+ componentsBySeverity: getComponentsBySeverity,
+ scan: getFullScan,
+ status: getStatus,
+ vulnerabilitiesByLayer: getVulnerabilitiesByLayer
+});
+
+/**
+ * This component is the detail view of particular scan/tag combination, showing
+ * vulnerability and component information for a tag.
+ */
+@connect(mapState)
+export default class ScannedTag extends Component {
+
+ static propTypes = {
+ componentsBySeverity: shape({
+ critical: array,
+ major: array,
+ minor: array,
+ secure: array
+ }),
+ scan: object,
+ status: instanceOf(Map),
+ vulnerabilitiesByLayer: object
+ }
+
+ mkLayer = (layerIndex) => {
+ const { scan, vulnerabilitiesByLayer } = this.props;
+ const layer = scan.layers[layerIndex];
+ const layerVulnerabilities = vulnerabilitiesByLayer[layerIndex];
+ let layerComponents = {};
+ //layer.components is an array with ids
+ forEach(layer.components, c => {
+ layerComponents[c] = scan.components[c];
+ });
+ return (
+
+ );
+ };
+
+ render() {
+ const {
+ componentsBySeverity,
+ params,
+ namespace,
+ scan,
+ status,
+ vulnerabilitiesByLayer
+ } = this.props;
+ const { blobs, reponame, tag, scan_id } = scan;
+ //TODO change to use redux-simple-router params when we include it
+ const ns = namespace ? namespace : params.user;
+ const rn = reponame ? reponame : params.splat;
+ const tn = tag ? tag : params.tagname;
+ //No scan_id ==> first scan has failed or is in progress
+ const scanError = !blobs || !scan_id;
+ if (status.getIn(['getScanForTag', ns, rn, tn, 'status']) === ERROR || scanError) {
+ return (
+
+
+
Scan results unavailable.
+
+
+ );
+ }
+ const layerInfo = {`${reponame}:${tag}`};
+ // blobs is an ordered array of layer ids, so we must use that to preserve API ordering
+ return (
+ Scan results for {layerInfo}}>
+
+
+
+ {blobs.map(this.mkLayer)}
+
+
+
+ );
+ }
+}
diff --git a/app/scripts/components/repo/repo_details/Tags.css b/app/scripts/components/repo/repo_details/Tags.css
new file mode 100644
index 0000000000..64cc8c458b
--- /dev/null
+++ b/app/scripts/components/repo/repo_details/Tags.css
@@ -0,0 +1,36 @@
+@import "dux/css/colors.css";
+
+.cardHeader {
+ font-size: 1.2rem;
+}
+
+.secondaryTableHeader {
+ font-weight: 500;
+ font-size: 0.9rem;
+ padding: 0.4rem 0;
+}
+
+.empty {
+ font-size: 1.25rem;
+ font-weight: 300;
+ color: var(--secondary-3);
+}
+
+.inlineBlock {
+ display: inline-block;
+}
+
+.questionMark {
+ composes: inlineBlock;
+ color: #C4CDD9;
+ cursor: pointer;
+ font-size: 1.2rem;
+}
+
+.tooltipTitle {
+ font-weight: 500;
+}
+
+.toggleButtonWrapper {
+ text-align: center;
+}
diff --git a/app/scripts/components/repo/repo_details/Tags.jsx b/app/scripts/components/repo/repo_details/Tags.jsx
new file mode 100644
index 0000000000..da20e5bd8e
--- /dev/null
+++ b/app/scripts/components/repo/repo_details/Tags.jsx
@@ -0,0 +1,212 @@
+'use strict';
+import React, { PropTypes, Component } from 'react';
+const { array, object, bool, string, number, shape, func } = PropTypes;
+import { connect } from 'react-redux';
+import Card, { Block } from '@dux/element-card';
+
+import { FlexTable, FlexRow, FlexHeader, FlexItem } from 'common/FlexTable.jsx';
+import ScannedTagRow from './tags/ScannedTagRow.jsx';
+import UnscannedTagRow from './tags/UnscannedTagRow.jsx';
+import styles from './Tags.css';
+import FontAwesome from 'common/FontAwesome';
+import Tooltip from 'rc-tooltip';
+import { createStructuredSelector } from 'reselect';
+import {
+ getScannedTags,
+ getScannedTagCount,
+ getUnscannedTags,
+ getUnscannedTagCount
+} from './tags/selectors';
+import { getStatus } from 'selectors/status';
+import * as tagActions from 'actions/redux/tags.js';
+import { mapActions } from 'reduxUtils';
+import Button from '@dux/element-button';
+import { StatusRecord } from 'records';
+import moment from 'moment';
+
+const debug = require('debug')('RepositoryDetailsTags');
+
+const mapState = createStructuredSelector({
+ scannedTags: getScannedTags,
+ scannedTagCount: getScannedTagCount,
+ unscannedTags: getUnscannedTags,
+ unscannedTagCount: getUnscannedTagCount,
+ status: getStatus
+});
+
+/**
+ * TagDisplay is the new UI for listing tags with vulnerability information from
+ * nautilus.
+ * It connects to the redux store and uses redux actions.
+ */
+@connect(mapState, mapActions(tagActions))
+class TagDisplay extends Component {
+
+ static propTypes = {
+ actions: shape({
+ deleteRepoTag: func
+ }),
+ status: object,
+
+ scannedTags: array,
+ scannedTagCount: number,
+ unscannedTags: array,
+ unscannedTagCount: number
+ }
+
+ state = {
+ //one of 'unknown', 'show'
+ showUnscannedTags: 'unknown'
+ }
+
+ toggleShowUnscannedTags = (e) => {
+ const { showUnscannedTags } = this.state;
+ //will be 'unknown' on the first time clicking Show Outdated Tags
+ this.setState({
+ showUnscannedTags: 'show'
+ });
+ }
+
+ mkUnscannedTagRow = (tag) => {
+ const { status } = this.props;
+ const tagName = tag.name;
+ const tagStatus = status.getIn(['deleteRepoTag', tagName], new StatusRecord());
+ return (
+
+ );
+ }
+
+ mkUnscannedTagTable = () => {
+ const { unscannedTags, unscannedTagCount, scannedTagCount } = this.props;
+ const { showUnscannedTags } = this.state;
+ if (!unscannedTagCount) {
+ return null;
+ }
+ //Nautilus scan results exist --> show the button instead of the table
+ if (showUnscannedTags === 'unknown' && scannedTagCount) {
+ return (
+
+
+
+ );
+ }
+ //Nautilus scan results exist and the button has been pressed to show unscanned tags
+ if (showUnscannedTags === 'show' && scannedTagCount) {
+ return (
+
+
+
+
+ );
+
+ }
+}
diff --git a/app/scripts/components/repo/repo_details/tags/selectors.js b/app/scripts/components/repo/repo_details/tags/selectors.js
new file mode 100644
index 0000000000..07cc2e5104
--- /dev/null
+++ b/app/scripts/components/repo/repo_details/tags/selectors.js
@@ -0,0 +1,65 @@
+'use strict';
+
+import { createSelector } from 'reselect';
+import { Map, List } from 'immutable';
+import filter from 'lodash/collection/filter';
+import size from 'lodash/collection/size';
+import map from 'lodash/collection/map';
+import values from 'lodash/object/values';
+
+
+// Returns all repository tags (unordered)
+export const getRepoTags = (state) => {
+ const reponame = state.repos.get('name', '');
+ const namespace = state.repos.get('namespace', '');
+ // We need to use toJS() to deeply convert tags from immutable to objects.
+ // We also return an array because getScannedTags and getUnscannedTags return
+ // arrays - keeping things consistent.
+ return values(state.tags.getIn([namespace, reponame, 'tags'], new Map()).toJS());
+};
+
+// Returns all repository tags in the order of the hub API
+// Note: This does _not_ return any tags that are returned by nautilus but not hub
+// so we use the getRepoTags for the scannedTag selector
+export const getRepoTagsInOrder = (state) => {
+ const reponame = state.repos.get('name', '');
+ const namespace = state.repos.get('namespace', '');
+ // We need to use toJS() to deeply convert tags from immutable to objects.
+ // We also return an array because getScannedTags and getUnscannedTags return
+ // arrays - keeping things consistent.
+ let orderedTags = state.tags.getIn([namespace, reponame, 'result'], []);
+ if (orderedTags.toArray) {
+ orderedTags = orderedTags.toArray();
+ }
+ const tags = state.tags.getIn([namespace, reponame, 'tags'], new Map()).toJS();
+ return map(orderedTags, (tagId) => tags[tagId]);
+};
+
+
+// Returns only tags which have been scanned by nautilus
+export const getScannedTags = createSelector(
+ [getRepoTags],
+ (tags) => {
+ // If the tag has a 'healthy' key then this has been scanned by nautilus
+ return filter(tags, (tag) => tag.healthy !== undefined);
+ }
+);
+// Number of tags scanned by nautilus
+export const getScannedTagCount = createSelector(
+ [getScannedTags],
+ (tags) => size(tags)
+);
+
+// getUnscannedTags returns only tags **not** scanned by nautilus
+export const getUnscannedTags = createSelector(
+ [getRepoTagsInOrder],
+ (tags) => {
+ // If healthy is undefined this tag only has a hub response
+ return filter(tags, (tag) => tag.healthy === undefined);
+ }
+);
+
+export const getUnscannedTagCount = createSelector(
+ [getUnscannedTags],
+ (tags) => size(tags)
+);
diff --git a/app/scripts/components/repositories/AutoBuildSetupForm.css b/app/scripts/components/repositories/AutoBuildSetupForm.css
new file mode 100644
index 0000000000..c078308f01
--- /dev/null
+++ b/app/scripts/components/repositories/AutoBuildSetupForm.css
@@ -0,0 +1,49 @@
+@import "dux/css/box.css";
+@import "dux/css/colors.css";
+
+.errorText {
+ white-space: pre;
+}
+
+.input {
+ margin-right: var(--default-margin);
+}
+
+.formContainer {
+ margin-top: var(--default-margin);
+}
+
+.error {
+ color: var(--primary-5);
+ font-size: .875rem;
+ margin-bottom: 0.3rem;
+}
+
+/* TODO: this is also used in EnterpriseTrialForm.css | a candidate to be in colors.css */
+.label {
+ color: #7a8491;
+ font-weight: 500;
+ sup {
+ font-size: 1rem;
+ vertical-align: text-bottom;
+ }
+}
+
+.customizeLabel {
+ composes: label;
+ margin-bottom: 0.5rem;
+}
+
+.floatRight {
+ float: right;
+ margin-right: 1rem;
+}
+
+.globalError {
+ composes: error;
+ composes: floatRight;
+}
+
+.select {
+ border-radius: var(--global-radius);
+}
\ No newline at end of file
diff --git a/app/scripts/components/repositories/AutoBuildSetupForm.jsx b/app/scripts/components/repositories/AutoBuildSetupForm.jsx
new file mode 100644
index 0000000000..19d03c80af
--- /dev/null
+++ b/app/scripts/components/repositories/AutoBuildSetupForm.jsx
@@ -0,0 +1,410 @@
+'use strict';
+
+import React, { PropTypes } from 'react';
+import findIndex from 'lodash/array/findIndex';
+import includes from 'lodash/collection/includes';
+import omit from 'lodash/object/omit';
+import map from 'lodash/collection/map';
+import AutoBuildTagsInput from './AutoBuildTagsInput.jsx';
+import connectToStores from 'fluxible-addons-react/connectToStores';
+import RepositoryNameInput from 'common/RepositoryNameInput.jsx';
+import SimpleTextArea from 'common/SimpleTextArea.jsx';
+import AutobuildStore from '../../stores/AutobuildStore';
+import AutobuildConfigStore from '../../stores/AutobuildConfigStore';
+import AutobuildSourceRepositoriesStore from '../../stores/AutobuildSourceRepositoriesStore';
+import RepoStore from '../../stores/RepositoryPageStore';
+import UserStore from '../../stores/UserStore';
+import createAutobuild from '../../actions/createAutobuild';
+import updateAutobuildFormField from '../../actions/updateAutobuildFormField.js';
+import getSettingsData from 'actions/getSettingsData';
+import { PageHeader } from 'dux';
+import AlertBox from 'common/AlertBox';
+import Card, { Block } from '@dux/element-card';
+import Button from '@dux/element-button';
+import { validateRepositoryName } from '../utils/validateRepositoryName';
+import { STATUS as COMMONSTATUS } from '../../stores/common/Constants';
+import Markdown from '@dux/element-markdown';
+
+const {
+ ATTEMPTING
+} = COMMONSTATUS;
+
+const buildTagsClientSideError = 'No empty strings allowed for docker tag (or) source tag/branch name specification.';
+
+import styles from './AutoBuildSetupForm.css';
+
+const {
+ array,
+ bool,
+ func,
+ number,
+ object,
+ oneOf,
+ shape,
+ string
+} = PropTypes;
+
+var AutoBuildSetupForm = React.createClass({
+ contextTypes: {
+ executeAction: func.isRequired
+ },
+ propTypes: {
+ user: object.isRequired,
+ JWT: string.isRequired,
+ ownedNamespaces: array.isRequired,
+ configStore: shape({
+ description: string,
+ isPrivate: oneOf(['private', 'public']).isRequired,
+ name: string.isRequired,
+ namespace: string.isRequired,
+ sourceRepoName: string.isRequired,
+ STATUS: string.isRequired
+ }),
+ sourceRepositories: shape({
+ type: string.isRequired
+ })
+ },
+ getInitialState: function() {
+ return {
+ isActive: true,
+ buildTags: this.defaultBuildTags,
+ clientSideError: '',
+ advancedMode: false
+ };
+ },
+ /*eslint-disable camelcase*/
+
+ /*
+ * By default, if the input is empty for source tag/branch name, send the string: '{sourceref}' & 'master'
+ * By default, if the input is empty for docker tag name, send the string with regex for all matches & 'latest'
+ */
+ defaultBuildTags: [
+ {
+ id: 'tag-0',
+ name: 'latest',
+ source_type: 'Branch',
+ source_name: 'master',
+ dockerfile_location: '/'
+ },
+ {
+ id: 'tag-1',
+ name: '{sourceref}',
+ source_type: 'Branch',
+ source_name: '/^([^m]|.[^a]|..[^s]|...[^t]|....[^e]|.....[^r]|.{0,5}$|.{7,})/',
+ dockerfile_location: '/'
+ }
+ ],
+ getBuildTagsToSend: function() {
+ let bTags = this.state.buildTags;
+ return map(bTags, (tag) => {
+ return omit(tag, 'id');
+ });
+ },
+ /*eslint-enable camelcase */
+ _handleCreate: function(evt) {
+ evt.preventDefault();
+ const { username } = this.props.user;
+ const { buildTags, isActive } = this.state;
+ const { description, isPrivate, name, namespace, sourceRepoName } = this.props.configStore;
+ const { type } = this.props.sourceRepositories;
+
+ const params = this.props.params;
+ const sourceRepoFallback = `${params.sourceRepoNamespace}/${params.sourceRepoName}`;
+
+ if (!validateRepositoryName(name.toLowerCase())) {
+ //check if the repo name is valid | client side check
+ this.setState({
+ clientSideError: `No spaces and special characters other than '.' and '-' are allowed.
+Repository names should not begin/end with a '.' or '-'.`
+ });
+ } else {
+
+ var newAutobuild = {
+ user: username,
+ namespace: namespace,
+ name: name.toLowerCase(),
+ description: description,
+ is_private: isPrivate === 'private',
+ build_name: sourceRepoName.toLowerCase() || sourceRepoFallback.toLowerCase(),
+ provider: type.toLowerCase(),
+ active: isActive,
+ tags: this.getBuildTagsToSend()
+ };
+
+ this.context.executeAction(createAutobuild, {JWT: this.props.JWT, autobuildConfig: newAutobuild});
+ }
+ },
+ _onActiveStateChange: function(e) {
+ this.setState({isActive: !this.state.isActive});
+ },
+ _getTagIndex: function(id) {
+ return findIndex(this.state.buildTags, function(tag) {
+ return (tag.id === id);
+ });
+ },
+ _setTagState: function(id, prop, value) {
+ let bTags = this.state.buildTags;
+ bTags[this._getTagIndex(id)][prop] = value; //find tag and update property
+ this.setState({
+ buildTags: bTags
+ });
+ },
+ _onTagRemoved: function(id) {
+ //Remove tag, when user removes it from the form
+ let bTags = this.state.buildTags;
+ bTags.splice(this._getTagIndex(id), 1);
+ this.setState({
+ buildTags: bTags
+ });
+ },
+ _onTagAdded: function(tag) {
+ let bTags = this.state.buildTags;
+ bTags.push(tag);
+ this.setState({
+ buildTags: bTags
+ });
+ },
+ _resetBuildTagsError: function() {
+ //Reset clientSideError on change
+ if (this.state.clientSideError === buildTagsClientSideError) {
+ this.setState({
+ clientSideError: ''
+ });
+ }
+ },
+ _onSourceNameChange: function(tagId, e) {
+ let sourceName = e.target.value;
+ let sourceType = this.state.buildTags[this._getTagIndex(tagId)].sourceType;
+ if (sourceName === '' && sourceType && sourceType === 'Branch') {
+ sourceName = '/^([^m]|.[^a]|..[^s]|...[^t]|....[^e]|.....[^r]|.{0,5}$|.{7,})/';
+ } else if (sourceName === '' && sourceType && sourceType === 'Tag') {
+ sourceName = '/.*/';
+ } else {
+ this._resetBuildTagsError();
+ //Strip trailing and leading spaces. If we end up with empty string, throw an error.
+ if (sourceName.trim() === '') {
+ this.setState({
+ clientSideError: buildTagsClientSideError
+ });
+ }
+ }
+ this._setTagState(tagId, 'source_name', sourceName.trim());
+ },
+ _onSourceTypeChange: function(tagId, e) {
+ this._setTagState(tagId, 'source_type', e.target.value);
+ },
+ _onDockerfileLocationChange: function(tagId, e) {
+ this._setTagState(tagId, 'dockerfile_location', e.target.value);
+ },
+ _onTagChange: function(tagId, e) {
+ let tagName = e.target.value;
+ if (tagName === '') {
+ tagName = '{sourceref}';
+ }
+ this._setTagState(tagId, 'name', tagName.trim());
+ },
+ _updateForm(fieldKey) {
+ return (e) => {
+ if (fieldKey === 'namespace') {
+ this.setState({
+ currentNamespace: e.target.value
+ });
+ this.context.executeAction(getSettingsData, {
+ JWT: this.props.JWT,
+ username: e.target.value,
+ repoType: 'autobuild'
+ });
+ } else if (fieldKey === 'name' && this.state.clientSideError) {
+ this.setState({
+ clientSideError: ''
+ });
+ }
+ this.context.executeAction(updateAutobuildFormField, {
+ fieldKey,
+ fieldValue: e.target.value
+ });
+ };
+ },
+ componentWillReceiveProps: function(nextProps) {
+ const { name, namespace, success, STATUS } = nextProps.configStore;
+ //If autobuild was created successfully
+ if (STATUS.SUCCESSFUL || success) {
+ this.props.history.pushState(null, `/r/${namespace}/${name.toLowerCase()}/`);
+ }
+ },
+ customTagsConfig: function() {
+ this.setState({
+ advancedMode: true,
+ buildTags: []
+ });
+ },
+ defaultTagsConfig: function() {
+ this.setState({
+ advancedMode: false,
+ buildTags: this.defaultBuildTags
+ });
+ },
+ render: function() {
+
+ const {
+ description,
+ error,
+ isPrivate,
+ name,
+ namespace,
+ success,
+ STATUS
+ } = this.props.configStore;
+
+ /* start error/success handling */
+ let maybeSuccess = ;
+ if (success) {
+ maybeSuccess = {success};
+ }
+
+ let nameError;
+ let nameErrorContent = error.dockerhub_repo_name;
+ if(nameErrorContent) {
+ nameError = nameErrorContent;
+ }
+
+ let descriptionError;
+ if(error.description) {
+ descriptionError = error.description;
+ }
+
+ let privateRepoError;
+ if(error.is_private) {
+ privateRepoError = error.is_private;
+ }
+
+ let buildTagsError;
+ if (error.buildTags) {
+ buildTagsError = error.buildTags;
+ }
+
+ let maybeError = null;
+ let errorDetail = error.detail || this.state.clientSideError;
+ if (errorDetail) {
+ maybeError = (
+
+
+ {errorDetail}
+
+
+ );
+ }
+ /* end error handling */
+
+ //Check if user has passed in namespace as query | verify if they have access to it
+ let currentUserNamespace = this.props.location.query.namespace;
+ if (!includes(this.props.ownedNamespaces, currentUserNamespace)) {
+ //If they don't have access to the namespace set in the query param ? then fallback to default namespace
+ currentUserNamespace = this.props.user.namespace;
+ }
+ let tagsConfigList = null;
+
+ if (this.state.advancedMode) {
+ tagsConfigList = (
+
+
+ );
+ }
+});
+module.exports = Search;
diff --git a/app/scripts/components/search/SearchBar.css b/app/scripts/components/search/SearchBar.css
new file mode 100644
index 0000000000..46c8b37690
--- /dev/null
+++ b/app/scripts/components/search/SearchBar.css
@@ -0,0 +1,17 @@
+@import "dux/css/box";
+
+input.searchInput {
+ background: #405165;
+ border: 1px solid #4c5968;
+ border-radius: var(--global-radius);
+ color: #fff;
+ padding-left: 24px;
+}
+
+.fa {
+ position: relative;
+ color: white;
+ max-width: 1rem;
+ top: -7px;
+ left: 6px;
+}
\ No newline at end of file
diff --git a/app/scripts/components/search/SearchBar.jsx b/app/scripts/components/search/SearchBar.jsx
new file mode 100644
index 0000000000..86994c2df3
--- /dev/null
+++ b/app/scripts/components/search/SearchBar.jsx
@@ -0,0 +1,84 @@
+'use strict';
+import React from 'react';
+import FluxibleMixin from 'fluxible-addons-react/FluxibleMixin';
+import SearchStore from '../../stores/SearchStore';
+import styles from './SearchBar.css';
+import FA from '../common/FontAwesome';
+
+var debug = require('debug')('COMPONENT:SearchBar');
+
+var _getQueryParams = function(state) {
+ //transition to will always have `q` appended as query param at the very least
+ //Other query params like: `s` -> sort by | `t=User` -> user | `t=Organization` -> Org | `f=official`
+ // `f=automated_builds` | `s=date_created`, `s=last_updated`, `s=alphabetical`, `s=stars`, `s=downloads`
+ // `s=pushes`
+ var queryParams = {
+ q: state.query || '',
+ page: state.page || 1,
+ isAutomated: state.isAutomated || 0,
+ isOfficial: state.isOfficial || 0,
+ starCount: state.starCount || 0,
+ pullCount: state.pullCount || 0
+ };
+ return queryParams;
+};
+
+var SearchBar = React.createClass({
+ mixins: [FluxibleMixin],
+ statics: {
+ storeListeners: [SearchStore]
+ },
+ contextTypes: {
+ getStore: React.PropTypes.func.isRequired
+ },
+ getDefaultProps() {
+ return {
+ placeholder: 'Search'
+ };
+ },
+ getInitialState: function() {
+ return this.context.getStore(SearchStore).getState();
+ },
+ //on Search Store Change
+ onChange: function() {
+ //When a search query has been submitted
+ var state = this.context.getStore(SearchStore).getState();
+ this.setState(state);
+ },
+ _handleQueryChange: function(event) {
+ event.preventDefault();
+ //Change page to number 1 when the query is changed
+ this.setState({
+ page: 1
+ });
+ this.setState({query: event.target.value});
+ },
+ _handleQuerySubmit: function(event) {
+ event.preventDefault();
+ //second parameter will be empty object always since we don't have /search/{?}/
+ //third param will be the query /search/?q=whatever&s=blah&f=bleh
+ this.props.history.pushState(null, '/search/', _getQueryParams(this.state));
+ },
+ render: function() {
+ var searchQuery = this.state.query;
+ var inputPlaceholder = this.props.placeholder;
+ return (
+
+
+
+ );
+ }
+});
+
+module.exports = SearchBar;
diff --git a/app/scripts/components/search/SearchResultItem.jsx b/app/scripts/components/search/SearchResultItem.jsx
new file mode 100644
index 0000000000..579c35becf
--- /dev/null
+++ b/app/scripts/components/search/SearchResultItem.jsx
@@ -0,0 +1,67 @@
+'use strict';
+
+import React from 'react';
+import Badge from '../Badge.jsx';
+import StatsComponent from '../StatsComponent.jsx';
+var debug = require('debug')('COMPONENT:SearchResultItem');
+
+//TODO: will go under the ul in item info, will be a bunch of key value pairs reused across
+//TODO: Logged out views will have the `owner/reponame` (think about this)
+//TODO: Star icon should be passed to badge as d-`iconname` where `d-` is for the docker font icons
+
+var SearchResultItem = React.createClass({
+ render: function() {
+ var resultItem = this.props.resultItem;
+
+ //Push badges based on result item
+ var badges = [];
+ var officialBadge =
;
+ var autobuildBadge =
;
+
+ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
+ if (resultItem.is_official) {
+ badges.push(officialBadge);
+ } else if (resultItem.is_automated) {
+ badges.push(autobuildBadge);
+ }
+
+ //TODO: repo_owner is null atm, since API performance degrades if we try to get it
+ //
+ );
+ }
+ }
+});
+
+export default connectToStores(SignupForm,
+ [SignupStore],
+ function({ getStore }, props) {
+ return getStore(SignupStore).getState();
+ });
diff --git a/app/scripts/components/welcome/_utils.js b/app/scripts/components/welcome/_utils.js
new file mode 100644
index 0000000000..9ac9e62c89
--- /dev/null
+++ b/app/scripts/components/welcome/_utils.js
@@ -0,0 +1,43 @@
+'use strict';
+import _ from 'lodash';
+
+export function handleFormErrors(ctx, rawValueObject) {
+
+ /**
+ * This function expects a ctx that has a `fields`
+ * object on the state, and a `_validate` function that
+ * returns an object `{ hasError: bool, error: string }`
+ *
+ * A valid component looks like:
+ *
+ * var Component = React.createClasse({
+ * getInitialState() {
+ * return {
+ * fields: {}
+ * }
+ * },
+ * _validate(key, value) {
+ * return {
+ * hasError: true,
+ * error: 'It\'s always wrong!'
+ * }
+ * }
+ * })
+ */
+
+ // shortcut keys for State
+ let fields = ctx.state.fields || {};
+
+ // loop through `rawValueObject`, validating values
+ _.forIn(rawValueObject, function(value, key) {
+ let { hasError, error } = ctx._validate(key, value);
+ fields[key] = fields[key] || {};
+ fields[key].hasError = hasError;
+ fields[key].error = error;
+ }, ctx);
+
+ // queue up the new State
+ ctx.setState({
+ fields
+ });
+}
diff --git a/app/scripts/fluxibleRouter.js b/app/scripts/fluxibleRouter.js
new file mode 100644
index 0000000000..27194954eb
--- /dev/null
+++ b/app/scripts/fluxibleRouter.js
@@ -0,0 +1,112 @@
+'use strict';
+
+import React from 'react';
+import createHashHistory from 'history/lib/createHashHistory';
+import { createRoutes, useRoutes, RoutingContext } from 'react-router';
+import { routes } from 'react-router/lib/PropTypes';
+var debug = require('debug')('FluxibleRouter');
+
+const { func, object } = React.PropTypes;
+
+/**
+ * A is a high-level API for automatically setting up
+ * a router that renders a with all the props
+ * it needs each time the URL changes.
+ */
+const FluxibleRouter = React.createClass({
+
+ propTypes: {
+ history: object,
+ children: routes,
+ routes, // alias for children
+ createElement: func,
+ onError: func,
+ onUpdate: func,
+ parseQueryString: func,
+ stringifyQuery: func
+ },
+
+ getInitialState() {
+ return {
+ firstRender: true,
+ location: null,
+ routes: null,
+ params: null,
+ components: null
+ };
+ },
+
+ handleError(error) {
+ if (this.props.onError) {
+ this.props.onError.call(this, error);
+ } else {
+ // Throw errors by default so we don't silently swallow them!
+ throw error; // This error probably occurred in getChildRoutes or getComponents.
+ }
+ },
+
+ //============================================================================
+ //ComponentWillMount is the only addition to this Router
+ //Needed in order to provide location/pathname to onUpdate from client.js
+ /* eslint-disable */
+ componentWillMount() {
+ let { history, children, routes, parseQueryString, stringifyQuery } = this.props;
+ let createHistory = history ? () => history : createHashHistory;
+
+ this.history = useRoutes(createHistory)({
+ routes: createRoutes(routes || children),
+ parseQueryString,
+ stringifyQuery
+ });
+
+ this._unlisten = this.history.listen((error, state) => {
+ if (error) {
+ this.handleError(error);
+ } else {
+ //Rendering page after setting state, here to make sure the data is loaded `onUpdate` before we show the page
+ if (this.state.firstRender) {
+ this.setState({
+ firstRender: false,
+ ...state
+ });
+ } else {
+ var _this = this;
+ this.props.onUpdate(state, function () {
+ _this.setState(state);
+ });
+ }
+ //End change in `onUpdate` related change to get client side rendering to behave like now
+ }
+ });
+ },
+ //============================================================================
+ componentWillUnmount() {
+ if (this._unlisten) {
+ this._unlisten();
+ }
+ },
+
+ render() {
+ let { location, routes, params, components } = this.state;
+ let { createElement } = this.props;
+
+ if (location == null) {
+ return null; // Async match
+ }
+
+ const routingProps = {
+ history: this.history,
+ createElement,
+ location,
+ routes,
+ params,
+ components
+ };
+
+ return ;
+ }
+
+});
+/*eslint-enable*/
+
+export default FluxibleRouter;
diff --git a/app/scripts/middlewares/sdk.js b/app/scripts/middlewares/sdk.js
new file mode 100644
index 0000000000..25606daf01
--- /dev/null
+++ b/app/scripts/middlewares/sdk.js
@@ -0,0 +1,93 @@
+'use strict';
+
+import { ATTEMPTING, ERROR, SUCCESS } from '../reduxConsts.js';
+
+/**
+ * SDK middleware for automatically calling SDK actions and storing request
+ * statuses.
+ *
+ * Example:
+ *
+ * someAction({ tagName }) => ({
+ * type: 'SOME_ACTION',
+ * meta: {
+ * sdk: {
+ * call: SDK.func, // SDK function to call
+ * args: ['args', 'for', 'func'] // Args to pass in to SDK function
+ * callback: (err, res) => ({}) // SDK callback
+ * statusKey: ['SOMETHING'] // Unique identifier for saving status in status reducer
+ * }
+ * }
+ * })
+ *
+ * NOTE: `statusKey` should be an array; the first item should namespace
+ * the action type and the second item should be unique to the particular
+ * record. For example, when deleting a tag:
+ *
+ * statusKey: ['deleteRepoTag', 'latest']
+ *
+ * Status will be stored in 'state.status.deleteRepoTag.latest'.
+ *
+ * NOTE: `args` does not include the SDK callback.
+ *
+ * For an example see actions/redux/tags.js
+ */
+
+// dispatchStatus takes the action and status of an SDK request and returns
+// a new action to Redux for tracking state.
+//
+// The 'data' parameter may be either the error or response body from the call
+const dispatchStatus = (action, status, data) => ({
+ type: `${action.type}_STATUS`,
+ payload: {
+ // Add in everything from the initial action payload. This lets us pass
+ // things such as namespaces and repo names to reducers which handle
+ // success states (when deleting a tag we need the namespace/repo/tag name)
+ ...action.payload,
+ statusKey: action.meta.sdk.statusKey,
+ status,
+ data
+ }
+});
+
+const sdkMiddleware = (store) => (next) => (action) => {
+ // If there's no meta.sdk in our action we don't need to process it with our
+ // middleware
+ if (typeof action !== 'object' || !action.meta || !action.meta.sdk) {
+ return next(action);
+ }
+
+ const { call, args, callback, statusKey } = action.meta.sdk;
+
+ if (!statusKey) {
+ throw new Error(`action.meta.sdk.statusKey is not defined for ${action.type}`);
+ }
+
+ // Wrap the callback with a function that automatically dispatches error
+ // states for the SDK call to Redux.
+ // Why: this eliminates the need to create error and success dispatches for every
+ // action we create, and standardizes the format of all status dispatches
+ const wrapped = (err, res) => {
+ if (err) {
+ next(dispatchStatus(action, ERROR, err));
+ // TODO: Dispatch that there was an error with this call.
+ } else {
+ next(dispatchStatus(action, SUCCESS, res));
+ }
+
+ // Ensure we call the original callback supplied for the SDK call.
+ if (callback) {
+ callback.apply(null, [err, res]);
+ }
+ };
+
+ // Dispatch that we're attempting the SDK call
+ next(dispatchStatus(action, ATTEMPTING));
+
+ // Make the SDK call here.
+ call.apply(null, [...args, wrapped]);
+
+ return next(action);
+};
+
+export default sdkMiddleware;
diff --git a/app/scripts/normalizers.js b/app/scripts/normalizers.js
new file mode 100644
index 0000000000..5b7ca86f53
--- /dev/null
+++ b/app/scripts/normalizers.js
@@ -0,0 +1,64 @@
+'use strict';
+
+import { Schema, arrayOf } from 'normalizr';
+
+// Key repositories by the 'reponame' attribute instead of the 'key' field so
+// that we can look up repositories by name in our reselect queries
+const repository = new Schema('repository', { idAttribute: 'reponame' });
+
+// The tag ID shouldn't be via key - it should be tagname
+const tag = new Schema('tag', {
+ idAttribute: (entity) => {
+ // The natuilus API uses entity.tag as the tagname whereas hub uses
+ // entity.name
+ return (entity.tag) ? entity.tag : entity.name;
+ }
+});
+// The scan ID shouldn't use the ID attribute; we need to be able to load
+// any scan by checking the repo:tag combination
+// TODO: Can we use the sha256sum here instead?
+const scan = new Schema('scan', {
+ idAttribute: (entity) => `${entity.reponame}:${entity.tag}`
+});
+// AKA layer
+const blob = new Schema('blob', { idAttribute: 'index' });
+// Key components by names to version numbers as they have no unique ID
+const component = new Schema('component', {
+ idAttribute: (entity) => `${entity.component}:${entity.version}`
+});
+const vulnerability = new Schema('vulnerability', { idAttribute: 'cve' });
+
+
+// A repository has many tags
+repository.define({
+ tags: arrayOf(tag)
+});
+
+// A tag has many blobs and many scans
+tag.define({
+ blobs: arrayOf(blob),
+ // NOTE: Right now a tag can only have the latest scan.
+ // In the future we'll allow tags to have many scans
+ scans: arrayOf(scan)
+});
+
+scan.define({
+ blobs: arrayOf(blob)
+});
+
+blob.define({
+ components: arrayOf(component)
+});
+
+component.define({
+ vulnerabilities: arrayOf(vulnerability)
+});
+
+export {
+ repository,
+ tag,
+ scan,
+ blob,
+ component,
+ vulnerability
+};
diff --git a/app/scripts/records.js b/app/scripts/records.js
new file mode 100644
index 0000000000..9a603ddf48
--- /dev/null
+++ b/app/scripts/records.js
@@ -0,0 +1,14 @@
+'use strict';
+
+import { Record } from 'immutable';
+
+// Records are the same as Maps but with accessors
+// and can only have these defined fields set.
+// USE: Instead of `shape`, in propTypes, we can use
+// status: instanceOf(StatusRecord)
+//
+// NOTE: All records should be defined in this file
+export const StatusRecord = new Record({
+ status: '',
+ error: undefined
+});
diff --git a/app/scripts/reducers/_utils.js b/app/scripts/reducers/_utils.js
new file mode 100644
index 0000000000..b47be097cf
--- /dev/null
+++ b/app/scripts/reducers/_utils.js
@@ -0,0 +1,45 @@
+'use strict';
+
+import Immutable from 'immutable';
+
+/**
+ * mergeEntity takes an entity name and merges it into the current state
+ * if found.
+ *
+ * This is used when your state contains a basic map of entities.
+ *
+ * Examples:
+ *
+ * mergeEntity('repository'):
+ * > merge action.payload.entities.repository into the current state
+ *
+ */
+export const mergeEntity = (entityType) => (state, action) => {
+ //TODO: Remove promises stuff with ready / error?
+ const { payload, ready, error } = action;
+ if (!ready || error || !payload.entities[entityType]) {
+ return state;
+ }
+ return state.merge(new Immutable.Map(payload.entities[entityType]));
+};
+
+export const mapToRecord = (map, Record) => {
+ let records = {};
+ Object.keys(map).forEach(item => { records[item] = new Record(map[item]); });
+ return records;
+};
+
+export const mergeEntities = (...entities) => (state, action) => {
+
+ const { payload, ready, error } = action;
+ if (!ready || error) {
+ return state;
+ }
+
+ return state.withMutations( map => {
+ entities.forEach( item => {
+ return map.mergeIn([item], new Immutable.Map(payload.entities[item]));
+ });
+ return map;
+ });
+};
diff --git a/app/scripts/reducers/index.js b/app/scripts/reducers/index.js
new file mode 100644
index 0000000000..fd33d66740
--- /dev/null
+++ b/app/scripts/reducers/index.js
@@ -0,0 +1,21 @@
+'use strict';
+
+// This combines all reducers from internal and external packages to create
+// a redux store.
+import { combineReducers } from 'redux';
+import repos from './repos';
+import scans from './scans';
+import status from './status';
+import tags from './tags';
+import { reducer as ui } from 'redux-ui';
+
+export default combineReducers({
+ // external reducers
+ ui,
+ // middleware reducers
+ // app-specific reducers
+ repos,
+ scans,
+ status,
+ tags
+});
diff --git a/app/scripts/reducers/repos.js b/app/scripts/reducers/repos.js
new file mode 100644
index 0000000000..d96a1fdcd1
--- /dev/null
+++ b/app/scripts/reducers/repos.js
@@ -0,0 +1,24 @@
+'use strict';
+
+import immutable from 'immutable';
+import {
+ RECEIVE_REPO
+} from 'reduxConsts.js';
+
+const defaultState = immutable.fromJS(
+ (typeof window !== 'undefined' && window.ReduxApp.repos) || {}
+);
+
+const reducers = {
+ [RECEIVE_REPO]: (state, action) => {
+ return state.clear().merge(action.payload);
+ }
+};
+
+export default function(state = defaultState, action) {
+ const { type } = action;
+ if (typeof reducers[type] === 'function') {
+ return reducers[type](state, action);
+ }
+ return state;
+}
diff --git a/app/scripts/reducers/scans.js b/app/scripts/reducers/scans.js
new file mode 100644
index 0000000000..e3074ad4e2
--- /dev/null
+++ b/app/scripts/reducers/scans.js
@@ -0,0 +1,26 @@
+'use strict';
+
+//modified from nautilus-ui/src/scripts/reducers/scans.js
+import immutable from 'immutable';
+import { RECEIVE_SCANNED_TAG_DATA } from 'reduxConsts.js';
+
+// Map of entities within each scan
+const defaultState = immutable.fromJS(
+ (typeof window !== 'undefined' && window.ReduxApp.scans) || {}
+);
+
+const reducers = {
+ [ RECEIVE_SCANNED_TAG_DATA ]: (state, action) => {
+ // Here we only ever save this current scan from the repoDetailsScannedTag
+ // action. This means that our scans reducer only ever has one scan - for
+ // the current page.
+ return state.clear().merge(action.payload.entities);
+ }
+};
+
+export default function(state = defaultState, action) {
+ if (typeof reducers[action.type] === 'function') {
+ return reducers[action.type](state, action);
+ }
+ return state;
+}
diff --git a/app/scripts/reducers/status.js b/app/scripts/reducers/status.js
new file mode 100644
index 0000000000..589187b83c
--- /dev/null
+++ b/app/scripts/reducers/status.js
@@ -0,0 +1,40 @@
+'use strict';
+
+import immutable, { Map } from 'immutable';
+import endsWith from 'lodash/string/endsWith';
+import isArray from 'lodash/lang/isArray';
+import { ERROR } from 'reduxConsts.js';
+import { StatusRecord } from 'records';
+
+const defaultState = immutable.fromJS(
+ (typeof window !== 'undefined' && window.ReduxApp.status) || {}
+);
+
+// This reducer listens for status updates from the SDK middleware
+// and automatically stores the status within the `statusKey`.
+//
+// Example: If statusKey = ['deleteRepoTag', 'latest'] and status = 'ATTEMPTING',
+// then state.status.deleteRepoTag.latest would be `ATTEMPTING`.
+export default function(state = defaultState, action) {
+ // The status reducer only acts on actions ending in _STATUS.
+ // Ignore anything else and return the default state.
+ if (!endsWith(action.type, `_STATUS`)) {
+ return state;
+ }
+
+ const { statusKey, status, data } = action.payload;
+ // We're using setIn, so if the statusKey is a string it needs
+ // to be wrapped in an array.
+ const sk = isArray(statusKey) ? statusKey : [statusKey];
+
+ if (status === ERROR) {
+ // Store the status and error response from the API within
+ // the state.
+ return state.setIn(sk, new StatusRecord({ status, error: data }));
+ }
+
+ // On ATTEMPTING or SUCCESS we only want to store the status;
+ // storing action.payload.data would store the entire API response
+ // which our other reducers should be handling.
+ return state.setIn(sk, new StatusRecord({ status }));
+}
diff --git a/app/scripts/reducers/tags.js b/app/scripts/reducers/tags.js
new file mode 100644
index 0000000000..cb933949cb
--- /dev/null
+++ b/app/scripts/reducers/tags.js
@@ -0,0 +1,91 @@
+'use strict';
+
+import immutable, { Map } from 'immutable';
+import {
+ RECEIVE_NAUTILUS_TAGS_FOR_REPOSITORY,
+ RECEIVE_TAGS_FOR_REPOSITORY,
+ DELETE_REPO_TAG,
+ SUCCESS
+} from 'reduxConsts.js';
+
+// Use the serialized redux data from the universal app loading if it exists.
+//
+// Shape of state:
+// {
+// 'namespace': {
+// 'reponame': {
+// tags: {
+// 'latest': { ...data },
+// '14.04': { ...data },
+// ...
+// },
+// result: [1, 2, 3, ...] // Array of repo IDs as ordered by hub API
+// },
+// ...
+// }
+// }
+//
+// We use a nested map of namespace:reponame keys to a list of tags to ensure
+// that we can merge the nautilus and hub API responses together without
+// clearing inbetween.
+const defaultState = immutable.fromJS(
+ (typeof window !== 'undefined' && window.ReduxApp.tags) || {}
+);
+
+// mergeTagsIntoState accepts a namespace, reponame and normalized tag
+// information and merges them into the given state.
+//
+// This is used when tags from the hub and nautilus API are loaded.
+const mergeTagsIntoState = (state, action) => {
+ const { namespace, reponame, tags } = action.payload;
+ const path = [namespace, reponame, 'tags'];
+ const { tag } = tags.entities;
+
+ return state.setIn(
+ path,
+ // Get the existing tags for this namespace/repo and merge the normalized
+ // tags recursively. If the namespace/repo pair doesn't exist this returns
+ // a new map.
+ //
+ // Merge function ensures that in the event of a conflict where the new value
+ // is undefined or null, it does not overwrite the existing value
+ state.getIn(path, new Map()).mergeDeep(tag)
+ );
+};
+
+const maybeDeleteTag = (state, action) => {
+ if (action.payload.status === SUCCESS) {
+ // Remove this tag from our reducer.
+ const { namespace, reponame, tagName } = action.payload;
+ return state.withMutations(s => {
+ let result = s.getIn([namespace, reponame, 'result']);
+ result = result.filter(tag => tag !== tagName);
+ s.deleteIn([namespace, reponame, 'tags', tagName]);
+ s.setIn([namespace, reponame, 'result'], result);
+ return s;
+ });
+ }
+ return state;
+};
+
+const reducers = {
+ [RECEIVE_TAGS_FOR_REPOSITORY]: (state, action) => {
+ // Add the result array of ordered tags from the hub API response,
+ // then merge tags in
+ const { namespace, reponame, tags } = action.payload;
+ const path = [namespace, reponame, 'result'];
+ const { result } = tags;
+ state = state.setIn(path, result);
+ return mergeTagsIntoState(state, action);
+ },
+ [RECEIVE_NAUTILUS_TAGS_FOR_REPOSITORY]: mergeTagsIntoState,
+ [`${DELETE_REPO_TAG}_STATUS`]: maybeDeleteTag
+};
+
+export default function(state = defaultState, action) {
+ const { type } = action;
+ if (typeof reducers[type] === 'function') {
+ return reducers[type](state, action);
+ }
+ return state;
+}
diff --git a/app/scripts/reduxConsts.js b/app/scripts/reduxConsts.js
new file mode 100644
index 0000000000..6ee2ee93b4
--- /dev/null
+++ b/app/scripts/reduxConsts.js
@@ -0,0 +1,19 @@
+'use strict';
+//Consts used for redux actions
+const keyMirror = require('keymirror');
+
+export default keyMirror({
+ //repos
+ RECEIVE_REPO: null,
+
+ //tags
+ DELETE_REPO_TAG: null,
+ RECEIVE_NAUTILUS_TAGS_FOR_REPOSITORY: null,
+ RECEIVE_SCANNED_TAG_DATA: null,
+ RECEIVE_TAGS_FOR_REPOSITORY: null,
+
+ // statuses
+ ATTEMPTING: null,
+ ERROR: null,
+ SUCCESS: null
+});
diff --git a/app/scripts/reduxStore.js b/app/scripts/reduxStore.js
new file mode 100644
index 0000000000..a5aaf0748f
--- /dev/null
+++ b/app/scripts/reduxStore.js
@@ -0,0 +1,38 @@
+'use strict';
+
+// This creates a new redux store by importing our reducers and middleware
+// and combining the two.
+import { applyMiddleware, compose, createStore } from 'redux';
+import { Iterable } from 'immutable';
+import reducers from './reducers';
+import sdkMiddleware from './middlewares/sdk.js';
+import createLogger from 'redux-logger';
+const debug = require('debug')('hub:redux:logger');
+
+
+// Logger must always be the last middleware in applyMiddleware
+const logger = createLogger({
+ predicate: () => process.env.ENV === `development`,
+ // Use the debug import as our logger
+ logger: {log: debug},
+ // Transform any immutableJS maps and iterables into their standard JS
+ // counterparts. This means you can inspect state within the console.
+ stateTransformer: (state) => {
+ let newState = {};
+ Object.keys(state).forEach(key => {
+ newState[key] = state[key];
+ if (Iterable.isIterable(state[key])) {
+ newState[key] = state[key].toJS();
+ }
+ });
+ return newState;
+ }
+});
+
+// Compose creates a new function by taking store enhancers (such as middleware
+// and any external enhancers) which modifies the createStore function.
+const enhancedCreateStore = compose(
+ applyMiddleware(sdkMiddleware, logger)
+)(createStore);
+
+export default enhancedCreateStore;
diff --git a/app/scripts/reduxUtils.js b/app/scripts/reduxUtils.js
new file mode 100644
index 0000000000..dfba193f92
--- /dev/null
+++ b/app/scripts/reduxUtils.js
@@ -0,0 +1,27 @@
+'use strict';
+
+import { bindActionCreators } from 'redux';
+import React from 'react';
+import consts from './reduxConsts';
+/**
+ * Given an object of actions, this returns a thunk which returns all actions
+ * bound to dispatch using the same key names.
+ *
+ * This allows us to use `this.props.actions.$actionName` within components
+ * after being connected to Redux.
+ *
+ * Example:
+ *
+ * @connect(mapState, mapActions(Actions))
+ * class Basic extends Component {
+ * // this.props.actions now containers all keys in Actions bound to dispatch
+ * }
+ *
+ */
+export let mapActions = (actions) => {
+ return (dispatch) => { return { actions: bindActionCreators(actions, dispatch) }; };
+};
+
+export const mapToArray = (map) => {
+ return Object.keys(map).map(key => map[key]);
+};
diff --git a/app/scripts/selectors/status.js b/app/scripts/selectors/status.js
new file mode 100644
index 0000000000..92b5d05e21
--- /dev/null
+++ b/app/scripts/selectors/status.js
@@ -0,0 +1,3 @@
+'use strict';
+
+export const getStatus = (state) => state.status;
diff --git a/app/scripts/server.js b/app/scripts/server.js
new file mode 100644
index 0000000000..edae4a4263
--- /dev/null
+++ b/app/scripts/server.js
@@ -0,0 +1,342 @@
+'use strict';
+var debug = require('debug')('hub:server');
+var path = require('path');
+if(process.env.NEW_RELIC_LICENSE_KEY && process.env.NEW_RELIC_APP_NAME) {
+ process.env.NEW_RELIC_NO_CONFIG_FILE = true;
+ require('newrelic');
+}
+var request = require('superagent');
+var bugsnag = require('bugsnag');
+
+import pick from 'lodash/object/pick';
+import merge from 'lodash/object/merge';
+import React from 'react';
+import { match } from 'react-router';
+import RoutingContext from 'react-router/lib/RoutingContext';
+import bootstrapCreateElement from './bootstrapCreateElement';
+import app from './app';
+import cookieParser from 'cookie-parser';
+import bodyParser from 'body-parser';
+import express from 'express';
+import favicon from 'serve-favicon';
+import navigateAction from './actions/navigate';
+import serialize from 'serialize-javascript';
+import HtmlComponent from './components/Html';
+
+import { Provider } from 'react-redux';
+import reducers from './reducers';
+import enhancedCreateStore from './reduxStore';
+
+const server = express();
+
+// don't broadcast we are using express
+server.disable('x-powered-by');
+
+// add bugsnag for asynch errors
+if (process.env.BUGSNAG_API_KEY) {
+ bugsnag.register(process.env.BUGSNAG_API_KEY);
+ server.use(bugsnag.requestHandler);
+}
+
+server.use(favicon('./favicon.ico'));
+server.use('/public', express.static('./public'));
+
+// Add a trailing '/' to the path if there is none
+server.use(function(req, res, next) {
+ if (req.path.substr(-1) !== '/' && req.path.length > 1) {
+ const query = req.url.slice(req.path.length);
+ res.redirect(301, req.path + '/' + query);
+ } else {
+ next();
+ }
+});
+
+// standard health check endpoint
+server.get('/_health', function (req, res) {
+ res.send('OK');
+});
+
+(function(){
+ const redirectToDockerPricing = function(req, res) {
+ res.redirect(301, 'https://www.docker.com/pricing');
+ };
+
+ const redirectTrialToDockerStore = function(req, res) {
+ res.redirect(301, 'https://store.docker.com/bundles/docker-datacenter/purchase?plan=free-trial');
+ };
+
+ server.get('/enterprise/trial/', redirectTrialToDockerStore); // redirect DDC Trial page to new Store page
+ server.get('/enterprise/', redirectToDockerPricing);
+ server.get('/subscriptions/', redirectToDockerPricing);
+})();
+
+server.get('/account/signup/', function(req, res, next) {
+ res.redirect('/');
+});
+
+server.get('/account/forgot-password/', function(req, res, next) {
+ res.redirect('/reset-password/');
+});
+
+server.get('/account/login/', function(req, res, next) {
+ res.redirect('/login/');
+});
+
+server.get('/_/', function(req, res, next) {
+ res.redirect('/explore/');
+});
+
+server.get('/official/', function(req, res, next) {
+ res.redirect('/explore/');
+});
+
+server.get('/account/accounts/', function(req, res, next) {
+ res.redirect('/account/authorized-services/');
+});
+
+server.get('/plans/', function(req, res, next) {
+ res.redirect('https://www.docker.com/pricing');
+});
+
+server.get('/resend-email-confirmation/', function(req, res, next) {
+ res.redirect('/reset-password/');
+});
+
+//There are two cases now:
+//Case 1: No query parameter, just the token | Default activation, with confirmation_key sent to the `activate` endpoint
+//Case 2: A `ref` query parameter is sent in the email validation URL | For partners, we send both key and ref to the activation endpoint
+server.get('/account/confirm-email/:token', function(req, res, next) {
+ if(req.params.token) {
+ //If on activate, we get a query parameter called `ref` back from the email link, we store it and send it with the POST
+ const { ref } = req.query;
+ const { token } = req.params;
+ var activateRequestBody = { confirmation_key: token };
+ if (ref) {
+ activateRequestBody.ref = ref;
+ }
+ request.post(`${process.env.REGISTRY_API_BASE_URL}/v2/users/activate/`)
+ .accept('application/json')
+ .send(activateRequestBody)
+ .end((err, apiRes) => {
+ if (err) {
+ debug('sign up error', err);
+ //Redirect to Login page for any error.
+ //We do not have generic error pages for 400s or 500s.
+ res.redirect('/login/');
+ } else if (!apiRes || !apiRes.body) {
+ debug('api response is empty');
+ //Redirect to Login page, when there is no response
+ //This is a care case that needs to be handled to make sure
+ //that it doesn't crash. See HUB-2094 for further details.
+ res.redirect('/login/');
+ } else {
+ const { redirect_url } = apiRes.body;
+ if (redirect_url) {
+ //Redirect to the redirect URL returned by the API
+ res.redirect(redirect_url);
+ } else {
+ //Fallback redirect to Login page, when there is no redirect URL
+ res.redirect('/login/');
+ }
+ }
+ });
+ } else {
+ res.redirect('/login/');
+ }
+});
+
+server.use(cookieParser());
+// 30 days in ms: 2592000000
+const expiry = 1000 * 60 * 60 * 24 * 30; // ms * s * m * h * days
+const cookieOpts = {
+ domain: process.env.COOKIE_DOMAIN,
+ httpOnly: true,
+ secure: true,
+ maxAge: expiry,
+ expires: new Date(Date.now() + expiry)
+};
+server.post('/attempt-login/',
+ bodyParser.json(),
+ function(req, res, next) {
+ res.cookie('token', req.body.jwt, cookieOpts);
+ res.end();
+});
+
+const isLoggedIn = function(req) {
+ return !!(req.cookies && (req.cookies.token || req.cookies.jwt));
+};
+
+server.get('/account/billing-plans/', function(req, res, next) {
+ if (!isLoggedIn(req)) {
+ return res.redirect('/billing-plans/');
+ }
+ next();
+});
+
+server.use(function(req, res, next) {
+ if (req.method !== 'GET') {
+ return next();
+ }
+ if (isLoggedIn(req) && (['/login/', '/reset-password/', '/register/'].indexOf(req.path) !== -1 || req.path.indexOf('/account/password-reset-confirm') === 0)) {
+ res.redirect('/');
+ } else if (!isLoggedIn(req) && req.path.indexOf('password-reset-confirm') === -1 && req.path.indexOf('/account/') === 0) {
+ res.redirect('/login/');
+ } else {
+ next();
+ }
+});
+
+server.post('/attempt-logout/', function(req, res, next) {
+ /**
+ * Delete the old cookie when we see it on logout.
+ */
+ const oldCookieOpts = merge({},
+ cookieOpts,
+ {
+ domain: '.docker.com'
+ });
+ res.clearCookie('jwt', oldCookieOpts);
+ res.clearCookie('token', cookieOpts);
+ res.end();
+});
+
+server.post('/oauth/github-attempt/',
+ bodyParser.json(),
+ function(req, res, next) {
+ res.cookie('ghOauthKey', req.body.ghk, cookieOpts);
+ res.end();
+});
+
+server.post('/oauth/github-done/', function(req, res, next) {
+ res.clearCookie('ghOauthKey', cookieOpts);
+ res.end();
+});
+
+server.use(function(req, res, next) {
+ // We may need to whitelist OPTIONS
+ if (req.method !== 'GET') {
+ res.end('This server does not respond to non-GET requests');
+ } else {
+ next();
+ }
+});
+
+server.use(function(req, res, next) {
+ // Within each request create a new Redux store from all of our reducers
+ // so that state is unique per request.
+ const store = enhancedCreateStore(reducers);
+ const context = app.createContext({
+ reduxStore: store
+ });
+
+ debug('context:', context, context.reduxStore);
+
+ //We get the Routes that have been created in the FluxibleComponent
+ const routes = app.getComponent();
+
+ const originalURL = req.originalUrl;
+ //We use the 'match' API to match the created routes with the current location (req.originalURL)
+ debug('matching route', originalURL);
+ match({ routes, location: originalURL }, (routerError, redirectLocation, renderProps) => {
+ // match uses createRoutes for history
+ //TODO: handle redirect, not found and errors
+ //TODO: need to handle generic 404s, 500s, 301s
+ //if (redirectLocation) {
+ //TODO: redirects need to be handled here
+ // res.redirect(301, redirectLocation.pathname + redirectLocation.search);
+ //}
+ //else if (error) {
+ //TODO: Render a nice 500 page with error displayed | HOPE THIS NEVER HAPPENS
+ // res.send(500, error.message);
+ //}
+ //else if (renderProps == null) {
+ //TODO: Probably render the 404 page here
+ // res.send(404, 'Not found');
+ //}
+
+ //If router errors out, bail
+ if (routerError) {
+ debug('Error in the Router', routerError);
+ res.end(routerError);
+ }
+ // whitelist cookies from express into renderProps
+ if (req.cookies) {
+ renderProps.cookies = pick(req.cookies, ['token', 'ghOauthKey']);
+ // For backward compat since we changed the cookie name
+ renderProps.cookies.jwt = renderProps.cookies.token;
+ }
+
+ // Set the props, so the server knows if the user is logged in
+ if (renderProps.cookies.jwt) {
+ renderProps.JWT = renderProps.cookies.jwt;
+ }
+
+ /**
+ * Execute navigate action to load data (we block the render until the data is
+ * completely loaded)
+ * You can see the actual server side render happens only after the
+ * `navigateAction` calls `done()` somewhere
+ */
+ context.executeAction(navigateAction, renderProps, function() {
+ debug('Exposing context state', context);
+ debug('EXPOSING RENDER PROPS', renderProps);
+ let serializedApp;
+ let reduxApp;
+ try {
+ /*
+ NOTE: If we have any html or request responses saved in the store
+ - serialize will not be able to parse this and will crash the node server
+ */
+ serializedApp = serialize(app.dehydrate(context));
+ reduxApp = serialize(store.getState());
+ } catch (err) {
+ debug('SERIALIZATION FAILURE: ', err);
+ }
+ const exposed = `window.App=${serializedApp}; window.ReduxApp = ${reduxApp};`;
+ debug('Rendering Application component into html');
+
+ // This is the Router 1.0.0 recommended way of doing server side rendering
+ // Also add a Provider around the routingContext for Redux.
+ // NOTE: We're defining our redux store above directly within the app context
+ const routingContext = (
+
+
+
+ );
+
+ debug('rendering html');
+ var html = React.renderToStaticMarkup(
+
+ );
+
+ res.send(html);
+ });
+ });
+});
+
+// add bugsnag for error handling middleware
+if(process.env.BUGSNAG_API_KEY) {
+ server.use(bugsnag.errorHandler);
+}
+
+// add generic error catching middleware so the server doesn't crash
+server.use(function catchError(err, req, res, next) {
+ const message = err.stack ? err.stack.replace(/\n/g, '') : '';
+ const errorLog = {
+ time: (new Date()).toISOString(),
+ service: 'hub-web-v2',
+ message
+ };
+ console.error(errorLog); // eslint-disable-line no-console
+});
+
+const port = process.env.PORT || 3000;
+
+// Stop the server if the process terminates
+const runningServer = server.listen(port, function onListen() {
+ process.on('exit', runningServer.close.bind(runningServer));
+ debug('Listening on port ' + port);
+});
diff --git a/app/scripts/stores/AccountInfoFormStore.js b/app/scripts/stores/AccountInfoFormStore.js
new file mode 100644
index 0000000000..0102a883c1
--- /dev/null
+++ b/app/scripts/stores/AccountInfoFormStore.js
@@ -0,0 +1,131 @@
+'use strict';
+
+var createStore = require('fluxible/addons/createStore');
+import { STATUS } from './common/Constants';
+var debug = require('debug')('AccountInfoFormStore');
+var _ = require('lodash');
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'AccountInfoFormStore',
+ handlers: {
+ ACCOUNT_INFO_CLEAR_FORM: '_clearForm',
+ ACCOUNT_INFO_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ ACCOUNT_INFO_ATTEMPT_START: '_attemptStart',
+ ACCOUNT_INFO_BAD_REQUEST: '_badRequest',
+ ACCOUNT_INFO_SUCCESS: '_success',
+ ACCOUNT_INFO_STATUS_CLEAR: '_clearStatus',
+ ACCOUNT_INFO_FACEPALM: '_facepalm',
+ RECEIVE_USER: '_receiveUser'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this.fields = {
+ full_name: {},
+ company: {},
+ location: {},
+ profile_url: {},
+ gravatar_email: {}
+ };
+
+ this.values = {
+ full_name: '',
+ company: '',
+ location: '',
+ profile_url: '',
+ gravatar_email: ''
+ };
+ },
+ _receiveUser(user) {
+ debug('receive user', user);
+ this.values.full_name = user.full_name;
+ this.values.company = user.company;
+ this.values.location = user.location;
+ this.values.profile_url = user.profile_url;
+ this.values.gravatar_email = user.gravatar_email;
+ this.emitChange();
+ },
+ _facepalm() {
+ // this happens if things are screwed and we can't recover gracefully
+ this.STATUS = STATUS.FACEPALM;
+ this.emitChange();
+ },
+ _clearForm() {
+ this.initialize();
+ this.emitChange();
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}) {
+ this.STATUS = STATUS.DEFAULT;
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _attemptStart() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.fields = {
+ full_name: {},
+ company: {},
+ location: {},
+ profile_url: {},
+ gravatar_email: {}
+ };
+ this.emitChange();
+ },
+ _success() {
+ this.STATUS = STATUS.SUCCESSFUL;
+ this.emitChange();
+ },
+ _clearStatus() {
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ /**
+ * This function expects keys which match the `this.fields` keys
+ * with an array of errors:
+ *
+ * {
+ * orgname: ['this field is required']
+ * }
+ */
+ let shouldEmitChange = false;
+ this.STATUS = STATUS.ERROR;
+
+ // cycle through the possible form fields
+ this.fields = _.mapValues(this.fields, function (errorObject, key) {
+ if(_.has(obj, key)) {
+ shouldEmitChange = true;
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+
+ if(shouldEmitChange) {
+ this.emitChange();
+ }
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ debug('rehydrate', state);
+ this.fields = state.fields;
+ this.values = state.values;
+ this.STATUS = state.STATUS;
+ }
+});
diff --git a/app/scripts/stores/AccountSettingsLicensesStore.js b/app/scripts/stores/AccountSettingsLicensesStore.js
new file mode 100644
index 0000000000..40274dddb7
--- /dev/null
+++ b/app/scripts/stores/AccountSettingsLicensesStore.js
@@ -0,0 +1,41 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('AccountSettingsLicenseStore');
+import _ from 'lodash';
+
+export default createStore({
+ storeName: 'AccountSettingsLicensesStore',
+ handlers: {
+ RECEIVE_LICENSES: '_receiveLicenses'
+ },
+ initialize: function() {
+ this.licenses = [];
+ this.attempting = true;
+ },
+ _receiveLicenses: function(licenses) {
+ this.licenses = _.flatten(licenses);
+ this.attempting = false;
+ this.emitChange();
+ },
+ getAttempt: function() {
+ return this.attempting;
+ },
+ setAttempt: function(flag) {
+ this.attempting = flag;
+ },
+ getState: function() {
+ return {
+ licenses: this.licenses,
+ attempting: this.attempting
+ };
+ },
+ rehydrate: function(state) {
+ this.licenses = state.licenses;
+ this.attempting = state.attempting;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
diff --git a/app/scripts/stores/AccountSettingsTeamsStore.js b/app/scripts/stores/AccountSettingsTeamsStore.js
new file mode 100644
index 0000000000..17b7d9a462
--- /dev/null
+++ b/app/scripts/stores/AccountSettingsTeamsStore.js
@@ -0,0 +1,29 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+
+export default createStore({
+ storeName: 'AccountSettingsTeamsStore',
+ handlers: {
+ RECEIVE_LICENSES: '_receiveLicenses'
+ },
+ initialize() {
+ this.licenses = [];
+ },
+ _receiveLicenses(res) {
+ this.licenses = res.results;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ licenses: this.licenses
+ };
+ },
+ rehydrate(state) {
+ this.licenses = state.licenses;
+ },
+ dehydrate() {
+ return this.getState();
+ }
+});
+
diff --git a/app/scripts/stores/AddOrganizationStore.js b/app/scripts/stores/AddOrganizationStore.js
new file mode 100644
index 0000000000..7fd9986b2d
--- /dev/null
+++ b/app/scripts/stores/AddOrganizationStore.js
@@ -0,0 +1,115 @@
+'use strict';
+
+var createStore = require('fluxible/addons/createStore');
+import { STATUS } from './addorganizationstore/Constants';
+var debug = require('debug')('AddOrganizationStore');
+var _ = require('lodash');
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'AddOrganizationStore',
+ handlers: {
+ ADD_ORG_CLEAR_FORM: '_clearForm',
+ ADD_ORG_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ ADD_ORG_ATTEMPT_START: '_addOrgAttemptStart',
+ ADD_ORG_BAD_REQUEST: '_badRequest',
+ ADD_ORG_SUCCESS: '_addOrgSuccess',
+ ADD_ORG_FACEPALM: '_facepalm',
+ CREATED_ORGANIZATION: '_clearForm',
+ CLEAR_ERRORS: '_clearErrors'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this.fields = {
+ gravatar_email: {},
+ orgname: {},
+ location: {},
+ company: {},
+ profile_url: {}
+ };
+
+ this.values = {
+ gravatar_email: '',
+ orgname: '',
+ location: '',
+ company: '',
+ profile_url: ''
+ };
+ },
+ _facepalm() {
+ // this happens if things are screwed and we can't recover gracefully
+ this.STATUS = STATUS.FACEPALM;
+ this.emitChange();
+ },
+ _clearForm() {
+ this.initialize();
+ this.emitChange();
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}) {
+ this._clearErrors(fieldKey);
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _attemptStart() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _signupSuccess() {
+ this.STATUS = STATUS.SUCCESSFUL_SIGNUP;
+ this.emitChange();
+ },
+ _clearErrors(fieldKey) {
+ if (this.STATUS === STATUS.BAD_REQUEST || this.STATUS === STATUS.FACEPALM) {
+ this.fields[fieldKey] = noErrorObj;
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ }
+ },
+ _badRequest(obj) {
+ /**
+ * This function expects keys which match the `this.fields` keys
+ * with an array of errors:
+ *
+ * {
+ * orgname: ['this field is required']
+ * }
+ */
+ let shouldEmitChange = false;
+ this.STATUS = STATUS.BAD_REQUEST;
+
+ // cycle through the possible form fields
+ this.fields = _.mapValues(this.fields, function (errorObject, key) {
+ if(_.has(obj, key)) {
+ shouldEmitChange = true;
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+
+ if(shouldEmitChange) {
+ this.emitChange();
+ }
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate() {
+ return {};
+ },
+ rehydrate(state) {
+ this.state = state;
+ }
+});
diff --git a/app/scripts/stores/AddTrialLicenseStore.js b/app/scripts/stores/AddTrialLicenseStore.js
new file mode 100644
index 0000000000..d448f26f50
--- /dev/null
+++ b/app/scripts/stores/AddTrialLicenseStore.js
@@ -0,0 +1,64 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { ATTEMPTING_DOWNLOAD,
+ BAD_REQUEST,
+ DEFAULT,
+ FACEPALM,
+ SUCCESSFUL_DOWNLOAD } from 'stores/addtriallicensestore/Constants';
+const debug = require('debug')('AddTrialLicenseStore');
+
+export default createStore({
+ storeName: 'AddTrialLicenseStore',
+ handlers: {
+ ATTEMPTING_LICENSE_DOWNLOAD_START: '_attemptingLicenseDownloadStart',
+ DOWNLOAD_LICENSE_CONTENT_BAD_REQUEST: '_downloadLicenseContentBadRequest',
+ DOWNLOAD_LICENSE_CONTENT_FACEPALM: '_facepalm',
+ RECEIVE_LICENSE_DOWNLOAD_CONTENT: '_receiveLicenseDownloadContent'
+ },
+ initialize: function() {
+ this.error = '';
+ this.STATUS = DEFAULT;
+ },
+ _attemptingLicenseDownloadStart: function() {
+ this.STATUS = ATTEMPTING_DOWNLOAD;
+ this.error = '';
+ this.emitChange();
+ },
+ _clearFeedbackStates: function() {
+ this.STATUS = DEFAULT;
+ this.error = '';
+ this.emitChange();
+ },
+ _downloadLicenseContentBadRequest: function(err) {
+ this.STATUS = BAD_REQUEST;
+ this.error = err;
+ this.emitChange();
+ },
+ _facepalm: function(err) {
+ this.STATUS = FACEPALM;
+ debug(err);
+ this.error = 'Sorry, an error occured and your license is unavailable at this time.';
+ this.emitChange();
+ },
+ _receiveLicenseDownloadContent: function() {
+ this.STATUS = SUCCESSFUL_DOWNLOAD;
+ this.error = '';
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ error: this.error,
+ STATUS: this.STATUS
+ };
+ },
+ rehydrate: function(state) {
+ this.error = state.error;
+ this.STATUS = state.STATUS;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
diff --git a/app/scripts/stores/AddWebhookFormStore.js b/app/scripts/stores/AddWebhookFormStore.js
new file mode 100644
index 0000000000..e315647c2f
--- /dev/null
+++ b/app/scripts/stores/AddWebhookFormStore.js
@@ -0,0 +1,86 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import last from 'lodash/array/last';
+import {
+ DEFAULT,
+ ATTEMPTING,
+ ERROR
+} from './addwebhookformstore/Constants';
+const debug = require('debug')('AddWebhookFormStore');
+
+var WebhooksSettingsStore = createStore({
+ storeName: 'AddWebhookFormStore',
+ handlers: {
+ RECEIVE_WEBHOOKS: '_receiveWebhooks',
+ ADD_WEBHOOK_CLEAR: '_clear',
+ ADD_WEBHOOK_START: '_start',
+ ADD_WEBHOOK_RESET: '_reset',
+ ADD_WEBHOOK_SUCCESS: '_success',
+ ADD_WEBHOOK_NEW_HOOK: '_newHook',
+ ADD_WEBHOOK_REMOVE_HOOK: '_removeHook',
+ ADD_WEBHOOK_ERROR: '_error',
+ ADD_WEBHOOK_MISSING_ARGS: '_handleMissingArgs',
+ ADD_WEBHOOK_VALIDATION_ERRORS: '_handleValidationErrors'
+ },
+ initialize() {
+ /**
+ * hookFields represent each `input` pairing that is
+ * rendered. They contain no data about the content of the input
+ */
+ this.hookFields = [1];
+ this.STATUS = DEFAULT;
+ this.serverErrors = {};
+ },
+ _error(args) {
+ // TODO: handle generic error
+ this.STATUS = ERROR;
+ this.serverErrors = args;
+ this.emitChange();
+ },
+ _handleMissingArgs(args) {
+ debug('missing args: ', args);
+ this.serverErrors = args;
+ },
+ _handleValidationErrors(args) {
+ debug('validation errors: ', args);
+ this.serverErrors = args;
+ },
+ _newHook() {
+ const { hookFields: fields } = this;
+ this.hookFields = fields.concat(last(fields) + 1);
+ this.emitChange();
+ },
+ _reset() {
+ this.initialize();
+ this.emitChange();
+ },
+ _start() {
+ this.STATUS = ATTEMPTING;
+ this.emitChange();
+ },
+ _success() {},
+ _receiveWebhooks(payload) {
+ debug(payload);
+ this.pipelines = payload.results;
+ this.emitChange();
+ },
+ _receiveAddWebhookErrors(error) {
+ },
+ getState() {
+ return {
+ STATUS: this.STATUS,
+ pipelines: this.pipelines,
+ hookFields: this.hookFields,
+ serverErrors: this.serverErrors
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.pipelines = state.pipelines;
+ this.hookFields = state.hookFields;
+ }
+});
+
+module.exports = WebhooksSettingsStore;
diff --git a/app/scripts/stores/ApplicationStore.js b/app/scripts/stores/ApplicationStore.js
new file mode 100644
index 0000000000..6b4ea63eb6
--- /dev/null
+++ b/app/scripts/stores/ApplicationStore.js
@@ -0,0 +1,33 @@
+'use strict';
+const createStore = require('fluxible/addons/createStore');
+
+var ApplicationStore = createStore({
+ storeName: 'ApplicationStore',
+ handlers: {
+ CHANGE_ROUTE: 'handleNavigate'
+ },
+ initialize: function() {
+ this.currentRoute = null;
+ },
+ handleNavigate: function(route) {
+ if (this.currentRoute && route.path === this.currentRoute.path) {
+ return;
+ }
+
+ this.currentRoute = route;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ route: this.currentRoute
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.currentRoute = state.route;
+ }
+});
+
+module.exports = ApplicationStore;
diff --git a/app/scripts/stores/AutoBuildSettingsStore.js b/app/scripts/stores/AutoBuildSettingsStore.js
new file mode 100644
index 0000000000..117c3c63e1
--- /dev/null
+++ b/app/scripts/stores/AutoBuildSettingsStore.js
@@ -0,0 +1,235 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import sortBy from 'lodash/collection/sortBy';
+import { STATUS } from './common/Constants';
+
+var AutoBuildSettingsStore = createStore({
+ storeName: 'AutoBuildSettingsStore',
+ handlers: {
+ AB_TRIGGER_BY_TAG_ERROR: '_triggerByTagError',
+ AB_TRIGGER_BY_TAG_SUCCESS: '_triggerByTagSuccess',
+ ATTEMPT_TRIGGER_BY_TAG: '_triggerByTagAttempt',
+ RECEIVE_AUTOBUILD_SETTINGS: '_receiveAutoBuildSettings',
+ UPDATE_AUTO_BUILD_SETTINGS: '_updateFields',
+ RECEIVE_AUTOBUILD_LINKS: '_receiveAutoBuildLinks',
+ LINK_AUTOBUILD_ERROR: '_linkAutoBuildError',
+ LINK_AUTOBUILD_SUCCESS: '_linkAutoBuildSuccess',
+ UPDATE_AUTOBUILD_PUSH_TRIGGER_ITEM: '_updateBuildTriggerItem',
+ UPDATE_AUTOBUILD_NEW_TAG_ITEM: '_updateBuildNewTagItem',
+ DELETE_AUTOBUILD_PUSH_TRIGGER_ITEM: '_deleteBuildTriggerItem',
+ DELETE_AUTOBUILD_NEW_TAG_ITEM: '_deleteBuildsNewTagItem',
+ ADD_AUTOBUILD_PUSH_TRIGGER_ITEM: '_addBuildTriggerItem',
+ SAVE_BUILD_TAGS_SUCCESS: '_saveTagsSuccess',
+ SAVE_BUILD_TAGS_ERROR: '_saveTagsError',
+ RECEIVE_TRIGGER_STATUS: '_receiveTriggerStatus',
+ RECEIVE_TRIGGER_LOGS: '_receiveTriggerLogs'
+ },
+ initialize: function() {
+ this.autoBuildStore = {
+ repository: '',
+ build_name: '',
+ provider: '',
+ source_url: '',
+ docker_url: '',
+ repo_web_url: '',
+ repo_type: '',
+ active: false,
+ deleted: false,
+ repo_id: '',
+ build_tags: [],
+ deploykey: null,
+ hook_id: ''
+ };
+ this.newTags = [];
+ this.validations = {
+ buildTags: {
+ hasError: false,
+ success: false,
+ errors: []
+ },
+ links: {
+ hasError: false,
+ success: false,
+ error: ''
+ },
+ trigger: {
+ success: '',
+ error: ''
+ }
+ };
+ this.autoBuildBlankSlate = {};
+ this.autoBuildLinks = [];
+ this.triggerLinkForm = {
+ repoName: ''
+ };
+ this.triggerStatus = {
+ token: '',
+ trigger_url: '',
+ active: false
+ };
+ this.triggerLogs = [];
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _resetValidations: function(field) {
+ if (field === 'buildTags') {
+ this.validations.buildTags = {
+ hasError: false,
+ success: false,
+ errors: []
+ };
+ } else {
+ this.validations[field] = {
+ hasError: false,
+ success: false,
+ error: ''
+ };
+ }
+ this.emitChange();
+ },
+ _receiveAutoBuildSettings: function(payload) {
+ this.autoBuildStore = payload;
+ const sorted = sortBy(payload.build_tags, 'id'); // ensure build_tags received are sorted
+ this.autoBuildStore.build_tags = sorted;
+ this.autoBuildBlankSlate = this.autoBuildStore;
+ this.emitChange();
+ },
+ _receiveAutoBuildLinks: function(payload) {
+ this.autoBuildLinks = payload;
+ this.triggerLinkForm.repoName = '';
+ this.emitChange();
+ },
+ _linkAutoBuildError: function() {
+ this.validations.links = {
+ hasError: true,
+ success: false,
+ error: 'Failed to link this repository to your Automated Build.'
+ };
+ this.emitChange();
+ },
+ _linkAutoBuildSuccess: function() {
+ this.validations.links = {
+ hasError: false,
+ success: true,
+ error: ''
+ };
+ this.emitChange();
+ },
+ _addBuildTriggerItem: function() {
+ this.newTags.push({
+ name: '',
+ dockerfile_location: '',
+ source_name: '',
+ source_type: 'Branch',
+ isNew: true
+ });
+ this._resetValidations('buildTags');
+ this.emitChange();
+ },
+ _deleteBuildTriggerItem: function(index) {
+ this.autoBuildStore.build_tags[index].toDelete = true;
+ this._resetValidations('buildTags');
+ this.emitChange();
+ },
+ _deleteBuildsNewTagItem: function(index) {
+ this.newTags[index].toDelete = true;
+ this._resetValidations('buildTags');
+ this.emitChange();
+ },
+ _updateBuildTriggerItem: function({ index, fieldkey, value}) {
+ this.autoBuildStore.build_tags[index][fieldkey] = value;
+ this._resetValidations('buildTags');
+ this.emitChange();
+ },
+ _updateBuildNewTagItem: function({ index, fieldkey, value}) {
+ this.newTags[index][fieldkey] = value;
+ this._resetValidations('buildTags');
+ this.emitChange();
+ },
+ _updateFields: function({ field, key, value }) {
+ this[field][key] = value;
+ if (field === 'triggerLinkForm') {
+ this._resetValidations('links');
+ }
+ this.emitChange();
+ },
+ _saveTagsSuccess: function() {
+ this.validations.buildTags = {
+ success: true,
+ hasError: false,
+ errors: []
+ };
+ this.newTags = [];
+ setTimeout(this._resetValidations.bind(this), 3000, 'buildTags');
+ this.emitChange();
+ },
+ _saveTagsError: function(tag) {
+ let currentErrors = this.validations.buildTags.errors;
+ if (tag.error) {
+ currentErrors.push(`${tag.name}: ${tag.error}`);
+ }
+ this.validations.buildTags = {
+ success: false,
+ hasError: true,
+ errors: currentErrors
+ };
+ this.emitChange();
+ },
+ _receiveTriggerStatus: function(triggerStatus){
+ this.triggerStatus = triggerStatus;
+ this.emitChange();
+ },
+ _receiveTriggerLogs: function(triggerLogs) {
+ this.triggerLogs = triggerLogs;
+ this.emitChange();
+ },
+ _triggerByTagAttempt: function() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _triggerByTagError: function(err) {
+ this.validations.trigger.error = err;
+ setTimeout(this._clearTriggerStatus.bind(this), 5000);
+ this.emitChange();
+ },
+ _triggerByTagSuccess: function(success) {
+ this.validations.trigger.success = success;
+ setTimeout(this._clearTriggerStatus.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearTriggerStatus: function() {
+ this.validations.trigger.success = '';
+ this.validations.trigger.error = '';
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ autoBuildStore: this.autoBuildStore,
+ autoBuildBlankSlate: this.autoBuildBlankSlate,
+ autoBuildLinks: this.autoBuildLinks,
+ autoTriggerForm: this.autoTriggerForm,
+ triggerStatus: this.triggerStatus,
+ triggerLinkForm: this.triggerLinkForm,
+ triggerLogs: this.triggerLogs,
+ validations: this.validations,
+ newTags: this.newTags,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.autoBuildStore = state.autoBuildStore;
+ this.autoBuildLinks = state.autoBuildLinks;
+ this.autoTriggerForm = state.autoTriggerForm;
+ this.triggerStatus = state.triggerStatus;
+ this.triggerLinkForm = state.triggerLinkForm;
+ this.triggerLogs = state.triggerLogs;
+ this.validations = state.validations;
+ this.newTags = state.newTags;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = AutoBuildSettingsStore;
diff --git a/app/scripts/stores/AutobuildConfigStore.js b/app/scripts/stores/AutobuildConfigStore.js
new file mode 100644
index 0000000000..b4f8190d18
--- /dev/null
+++ b/app/scripts/stores/AutobuildConfigStore.js
@@ -0,0 +1,134 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import forEach from 'lodash/collection/forEach';
+import isString from 'lodash/lang/isString';
+import { STATUS } from './common/Constants';
+
+var AutobuildConfigStore = createStore({
+ storeName: 'AutobuildConfigStore',
+ handlers: {
+ ATTEMPTING_AUTOBUILD_CREATION: '_autobuildCreateAttempt',
+ AUTOBUILD_ERROR: '_autobuildConfigError',
+ AUTOBUILD_BAD_REQUEST: '_autobuildBadRequest',
+ AUTOBUILD_UNAUTHORIZED: '_autobuildUnauthorized',
+ AUTOBUILD_SUCCESS: '_autobuildSuccess',
+ AUTOBUILD_FORM_UPDATE_FIELD_WITH_VALUE: '_updateFormField',
+ SELECT_SOURCE_REPO: '_selectSourceRepo',
+ CLEAR_AUTOBUILD_FORM_ERRORS: '_clearErrorStates',
+ INITIALIZE_AUTOBUILD_FORM: '_initializeForm',
+ RECEIVE_PRIVATE_REPOSTATS: '_getPrivateDefault'
+ },
+ initialize: function() {
+ this.name = '';
+ this.namespace = '';
+ this.description = '';
+ this.isPrivate = 'public';
+ this.provider = '';
+ this.sourceRepoName = '';
+ this.active = true;
+ this.error = {};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _autobuildConfigError: function(err) {
+ //TODO: handle config error here
+ this.error.general = 'An error occurred while configuring your automated build. Please try again later.';
+ setTimeout(this._clearErrorStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _autobuildCreateAttempt: function() {
+ this.error.buildTags = '';
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _autobuildBadRequest: function(err) {
+ forEach(err, (val, key) => {
+ this.error[key] = val.toString();
+ });
+
+ //For build_tags, make it a global error
+ if (err.build_tags) {
+ this.error.buildTags = 'Invalid character(s) provided in build tags configuration. Please check your input.';
+ }
+
+ if (err.detail || isString(err)) {
+ this.error.detail = err.detail || err;
+ }
+
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _autobuildUnauthorized: function(err) {
+ this.error.general = 'You have no permissions to create an automated build in this namespace.';
+ setTimeout(this._clearErrorStates.bind(this), 5000);
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _autobuildSuccess: function(err) {
+ this.success = 'Successfully configured an automated build repository.';
+ this.STATUS = STATUS.SUCCESSFUL;
+ setTimeout(this._clearErrorStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearErrorStates: function() {
+ this.error = {};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _getPrivateDefault: function(stats) {
+ this.isPrivate = stats.default_repo_visibility;
+ },
+ _initializeForm: function({ name, namespace }) {
+ this.name = name;
+ this.namespace = namespace;
+ this.description = '';
+ },
+ _selectSourceRepo: function(repo) {
+ this.sourceRepoName = repo.full_name;
+ this.emitChange();
+ },
+ _updateFormField: function({fieldKey, fieldValue}) {
+ this[fieldKey] = fieldValue;
+ if (fieldKey === 'name' || fieldKey === 'namespace') {
+ delete this.error.dockerhub_repo_name;
+ }
+ if (fieldKey === 'description') {
+ delete this.error.description;
+ }
+ delete this.error.detail;
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ name: this.name,
+ namespace: this.namespace,
+ description: this.description,
+ isPrivate: this.isPrivate,
+ provider: this.provider,
+ sourceRepoName: this.sourceRepoName,
+ active: this.active,
+ error: this.error,
+ success: this.success,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.name = state.name;
+ this.namespace = state.namespace;
+ this.description = state.description;
+ this.isPrivate = state.isPrivate;
+ this.provider = state.provider;
+ this.sourceRepoName = state.sourceRepoName;
+ this.active = state.active;
+ this.error = state.error;
+ this.success = state.success;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = AutobuildConfigStore;
diff --git a/app/scripts/stores/AutobuildSourceRepositoriesStore.js b/app/scripts/stores/AutobuildSourceRepositoriesStore.js
new file mode 100644
index 0000000000..2b2448afb4
--- /dev/null
+++ b/app/scripts/stores/AutobuildSourceRepositoriesStore.js
@@ -0,0 +1,46 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var AutobuildSourceRepositoriesStore = createStore({
+ storeName: 'AutobuildSourceRepositoriesStore',
+ handlers: {
+ RECEIVE_LINKED_REPO_SOURCES: '_receiveLinkedRepos',
+ LINKED_REPO_SOURCES_ERROR: '_linkedReposError',
+ SET_LINKED_REPO_TYPE: '_setType'
+ },
+ initialize: function() {
+ this.repos = [];
+ this.type = '';
+ this.error = '';
+ },
+ _setType: function(type) {
+ this.type = type;
+ this.emitChange();
+ },
+ _receiveLinkedRepos: function(linkedRepos) {
+ this.repos = linkedRepos;
+ this.emitChange();
+ },
+ _linkedReposError: function(err) {
+ this.error = 'Please check if you have any repositories setup on ' + this.type + '.';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ repos: this.repos,
+ type: this.type,
+ error: this.error
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.repos = state.repos;
+ this.type = state.type;
+ this.error = state.error;
+ }
+});
+
+module.exports = AutobuildSourceRepositoriesStore;
diff --git a/app/scripts/stores/AutobuildStore.js b/app/scripts/stores/AutobuildStore.js
new file mode 100644
index 0000000000..b012f99a60
--- /dev/null
+++ b/app/scripts/stores/AutobuildStore.js
@@ -0,0 +1,54 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var AutobuildStore = createStore({
+ storeName: 'AutobuildStore',
+ handlers: {
+ RECEIVE_SOURCE_REPOS: '_receiveSourceRepos',
+ RECEIVE_SOURCE_ACCOUNTS: '_receiveSourceAccount'
+ },
+ initialize: function() {
+ this.githubAccount = null;
+ this.githubRepos = [];
+ this.bitbucketAccount = null;
+ this.bitbucketRepos = [];
+ this.gitlabAccount = null;
+ this.gitlabRepos = [];
+ },
+ _receiveSourceRepos: function(res) {
+ this.githubRepos = res.github.detail ? [] : res.github;
+ this.bitbucketRepos = res.bitbucket.detail ? [] : res.bitbucket;
+ this.gitlabRepos = res.gitlab.detail ? [] : res.gitlab;
+ this.emitChange();
+ },
+ _receiveSourceAccount: function(res) {
+ this.githubAccount = res.github.detail ? null : res.github;
+ this.bitbucketAccount = res.bitbucket.detail ? null : res.bitbucket;
+ this.gitlabAccount = res.gitlab.detail ? null : res.gitlab;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ githubAccount: this.githubAccount,
+ githubRepos: this.githubRepos,
+ bitbucketAccount: this.bitbucketAccount,
+ bitbucketRepos: this.bitbucketRepos,
+ gitlabAccount: this.gitlabAccount,
+ gitlabRepos: this.gitlabRepos
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.githubAccount = state.githubAccount;
+ this.githubRepos = state.githubRepos;
+ this.bitbucketAccount = state.bitbucketAccount;
+ this.bitbucketRepos = state.bitbucketRepos;
+ this.gitlabAccount = state.gitlabAccount;
+ this.gitlabRepos = state.gitlabRepos;
+ }
+});
+
+module.exports = AutobuildStore;
diff --git a/app/scripts/stores/AutobuildTagsStore.js b/app/scripts/stores/AutobuildTagsStore.js
new file mode 100644
index 0000000000..56629f1ca7
--- /dev/null
+++ b/app/scripts/stores/AutobuildTagsStore.js
@@ -0,0 +1,50 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var AutobuildTagsStore = createStore({
+ storeName: 'AutobuildTagsStore',
+ handlers: {
+ AUTOBUILD_TAGS_ERROR: '_autobuildTagsError'
+ },
+ initialize: function() {
+ //tags is an array of tag
+ //{ dockerfile_location, source_type['Tag' or 'Branch'], source_name[eg. master] }
+ this.tags = [];
+ },
+ addTag: function(tag) {
+ //tag
+ //{
+ // id: 'row-1'
+ // sourceName: 'master'
+ // fileLocation: '/'
+ // buildTag: 'latest',
+ // sourceType: 'Branch'
+ //}
+ this.tags.push(tag);
+ },
+ removeTag: function(id) {
+ _.remove(this.tags, function(tag) {
+ return (tag.id === id);
+ });
+ },
+ setTagState: function(id, state) {
+ var tagToUpdate = _.find(this.tags, function(tag) {
+ return tag.id === id;
+ });
+ _.merge(tagToUpdate, state);
+ },
+ getState: function() {
+ return {
+ tags: this.tags
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.tags = state.tags;
+ }
+});
+
+module.exports = AutobuildTagsStore;
diff --git a/app/scripts/stores/AutobuildTriggerByTagStore.js b/app/scripts/stores/AutobuildTriggerByTagStore.js
new file mode 100644
index 0000000000..b3c16f81a1
--- /dev/null
+++ b/app/scripts/stores/AutobuildTriggerByTagStore.js
@@ -0,0 +1,91 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import findIndex from 'lodash/array/findIndex';
+import map from 'lodash/collection/map';
+import { STATUS } from './common/Constants';
+
+var AutobuildTriggerByTagStore = createStore({
+ storeName: 'AutobuildTriggerByTagStore',
+ handlers: {
+ INITIALIZE_AB_TRIGGERS: '_initTriggers',
+ AB_TRIGGER_BY_TAG_ERROR: '_triggerByTagError',
+ AB_TRIGGER_BY_TAG_SUCCESS: '_triggerByTagSuccess',
+ ATTEMPT_TRIGGER_BY_TAG: '_triggerByTagAttempt'
+ },
+ initialize: function() {
+ this.triggers = [];
+ this.tagStatuses = [];
+ },
+ _initTriggers: function(tags) {
+ //on load of the build settings page
+ this.initialize();
+
+ this.triggers = map(tags, (tag) => {
+ return {
+ id: tag.id,
+ success: '',
+ error: ''
+ };
+ });
+
+ this.tagStatuses = map(tags, (tag) => {
+ return {
+ id: tag.id,
+ status: STATUS.DEFAULT
+ };
+ });
+ this.emitChange();
+ },
+ _findIndices: function(id) {
+ const statusIndex = findIndex(this.tagStatuses, (s) => {
+ return s.id === id;
+ });
+ const triggerIndex = findIndex(this.triggers, (t) => {
+ return t.id === id;
+ });
+ return {statusIndex, triggerIndex};
+ },
+ _triggerByTagAttempt: function(id) {
+ const {statusIndex, triggerIndex} = this._findIndices(id);
+ this.tagStatuses[statusIndex].status = STATUS.ATTEMPTING;
+ this.triggers[triggerIndex].error = '';
+ this.triggers[triggerIndex].success = '';
+ this.emitChange();
+ },
+ _triggerByTagError: function(errObj) {
+ const {id, error} = errObj;
+ const { triggerIndex } = this._findIndices(id);
+ this.triggers[triggerIndex].error = error;
+ setTimeout(this._clearTriggerStatus.bind(this, id), 3000);
+ this.emitChange();
+ },
+ _triggerByTagSuccess: function(successObj) {
+ const {id, success} = successObj;
+ const { triggerIndex } = this._findIndices(id);
+ this.triggers[triggerIndex].success = success;
+ setTimeout(this._clearTriggerStatus.bind(this, id), 3000);
+ this.emitChange();
+ },
+ _clearTriggerStatus: function(id) {
+ const {statusIndex, triggerIndex} = this._findIndices(id);
+ this.tagStatuses[statusIndex].status = STATUS.DEFAULT;
+ this.triggers[triggerIndex].error = '';
+ this.triggers[triggerIndex].success = '';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ triggers: this.triggers,
+ tagStatuses: this.tagStatuses
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.triggers = state.triggers;
+ this.tagStatuses = state.tagStatuses;
+ }
+});
+
+module.exports = AutobuildTriggerByTagStore;
diff --git a/app/scripts/stores/BillingInfoFormStore.js b/app/scripts/stores/BillingInfoFormStore.js
new file mode 100644
index 0000000000..19473595c5
--- /dev/null
+++ b/app/scripts/stores/BillingInfoFormStore.js
@@ -0,0 +1,184 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+import { STATUS } from './billingformstore/Constants';
+var debug = require('debug')('STORE::BillingInfoFormStore');
+
+var BillingInfoFormStore = createStore({
+ storeName: 'BillingInfoFormStore',
+ handlers: {
+ BILLING_ACCOUNT_EXISTS: '_accountExists',
+ BILLING_INFO_EXISTS: '_billingInfoExists',
+ BILLING_ERRORS: '_updateErrors',
+ BILLING_INFO_UPDATE_FIELD_WITH_VALUE: '_updateBillingInfoForm',
+ BILLING_SUBMIT_ERROR: '_submitErrors',
+ BILLING_SUBMIT_START: '_submitStart',
+ BILLING_SUBMIT_SUCCESS: '_submitSuccess',
+ CLEAR_BILLING_FORM: '_clearBillingForm',
+ GET_RECURLY_ERROR: '_updateRecurlyErrors',
+ LOGOUT: '_clearStore',
+ RECEIVE_BILLING_INFO: '_receiveBillingInfo',
+ UPDATE_COUPON_VALUE: '_updateCouponValue'
+ },
+ initialize: function() {
+ var D = new Date();
+ var month = 1;
+ var year = D.getFullYear();
+ this.billforwardId = '';
+ this.accountInfo = { // Billing profile account
+ account_code: '',
+ username: '',
+ email: '',
+ first_name: '',
+ last_name: '',
+ company_name: '',
+ hasError: false,
+ newBilling: true
+ };
+ this.billingInfo = { // Billing card information
+ first_name: '',
+ last_name: '',
+ address1: '',
+ address2: '',
+ country: '',
+ state: '',
+ zip: '',
+ city: '',
+ last_four: '',
+ card_type: '',
+ month: '',
+ year: '',
+ newBilling: true
+ };
+ this.card = {
+ number: '',
+ cvv: '',
+ month: month,
+ year: year,
+ last_four: null,
+ type: '',
+ coupon_code: '',
+ coupon: 0
+ };
+ this.errorMessage = '';
+ this.fieldErrors = {
+ number: false,
+ expiry: false,
+ cvv: false,
+ coupon_code: false,
+ first_name: false,
+ last_name: false,
+ address1: false,
+ country: false,
+ state: false,
+ zip: false,
+ city: false,
+ month: false,
+ year: false
+ };
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _accountExists: function() {
+ this.accountInfo.newBilling = false;
+ this.emitChange();
+ },
+ _billingInfoExists: function() {
+ this.billingInfo.newBilling = false;
+ this.emitChange();
+ },
+ _clearStore: function() {
+ this.initialize();
+ },
+ _receiveBillingInfo: function(payload) {
+ this.initialize();
+ if (payload.billingInfo && payload.billingInfo.last_four) {
+ var cardInfo = {
+ last_four: payload.billingInfo.last_four,
+ type: payload.billingInfo.card_type,
+ month: payload.billingInfo.month,
+ year: payload.billingInfo.year
+ };
+ }
+ _.merge(this.billingInfo, payload.billingInfo);
+ _.merge(this.accountInfo, payload.accountInfo);
+ _.merge(this.card, cardInfo);
+ this.billforwardId = payload.billforwardId;
+ this.emitChange();
+ },
+ _submitErrors: function(message) {
+ this.STATUS = STATUS.FORM_ERROR;
+ this.errorMessage = message;
+ this.emitChange();
+ },
+ _submitSuccess: function() {
+ this.STATUS = STATUS.SUCCESS;
+ this.errorMessage = '';
+ this.emitChange();
+ },
+ _submitStart: function() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.errorMessage = '';
+ this.emitChange();
+ },
+ _updateBillingInfoForm: function({ field, fieldKey, fieldValue }) {
+ if (field === 'billing') {
+ this.billingInfo[fieldKey] = fieldValue;
+ } else if (field === 'account') {
+ this.accountInfo[fieldKey] = fieldValue;
+ } else if (field === 'card') {
+ this.card[fieldKey] = fieldValue;
+ }
+ this.emitChange();
+ },
+ _updateErrors: function(hasError) {
+ this.STATUS = STATUS.FORM_ERROR;
+ _.merge(this.fieldErrors, hasError.fieldErrors);
+ _.merge(this.accountInfo, hasError.accountErr);
+ this.errorMessage = 'Please make sure all fields are valid.';
+ this.emitChange();
+ },
+ _updateRecurlyErrors: function(error) {
+ const errorFields = error.fields;
+ debug('Recurly Form errors', errorFields);
+ const fieldErrors = {
+ number: _.includes(errorFields, 'number'),
+ expiry: _.includes(errorFields, 'month') || _.includes(errorFields, 'year'),
+ cvv: _.includes(errorFields, 'cvv'),
+ first_name: _.includes(errorFields, 'first_name'),
+ last_name: _.includes(errorFields, 'last_name')
+ };
+ _.merge(this.fieldErrors, fieldErrors);
+ this.STATUS = STATUS.FORM_ERROR;
+ this.errorMessage = error.message;
+ this.emitChange();
+ },
+ _updateCouponValue: function(value) {
+ this.card.coupon = value;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ billforwardId: this.billforwardId,
+ accountInfo: this.accountInfo,
+ billingInfo: this.billingInfo,
+ card: this.card,
+ errorMessage: this.errorMessage,
+ fieldErrors: this.fieldErrors,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.billforwardId = state.billforwardId;
+ this.accountInfo = state.accountInfo;
+ this.billingInfo = state.billingInfo;
+ this.card = state.card;
+ this.errorMessage = state.errorMessage;
+ this.fieldErrors = state.fieldErrors;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = BillingInfoFormStore;
diff --git a/app/scripts/stores/BillingPlansStore.js b/app/scripts/stores/BillingPlansStore.js
new file mode 100644
index 0000000000..3a25234628
--- /dev/null
+++ b/app/scripts/stores/BillingPlansStore.js
@@ -0,0 +1,128 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var BillingPlansStore = createStore({
+ storeName: 'BillingPlansStore',
+ handlers: {
+ RESET_BILLING_PLANS: '_clearStore',
+ RECEIVE_BILLING_INFO: '_receiveBillingInfo',
+ RECEIVE_BILLING_SUBSCRIPTION: '_receiveBillingSubscription',
+ RECEIVE_INVOICES: '_receiveInvoices',
+ RESET_CURRENT_PLAN: '_resetCurrentPlan',
+ UPDATE_PLAN_START: '_updatePlanStart',
+ UPDATE_PLAN_ERROR: '_updatePlanErr',
+ DELETE_SUBSCRIPTION_ERR: '_updatePlanErr',
+ DELETE_SUBSCRIPTION_SUCCESS: '_unsubscribeComplete',
+ UNSUBSCRIBE_SUBSCRIPTION: '_unsubscribe', // UNUSED - deprecated
+ UNSUBSCRIBE_PACKAGE: '_unsubscribePackage', // UNUSED - deprecated
+ UNSUBSCRIBE_PLAN: '_unsubscribePlan', // UNUSED - deprecated
+ LOGOUT: '_clearStore'
+ },
+ initialize: function() {
+ this.currentPlan = {};
+ this.accountInfo = {
+ account_code: '',
+ username: '',
+ email: '',
+ first_name: '',
+ last_name: '',
+ company_name: '',
+ hasError: false,
+ newBilling: true
+ };
+ this.billingInfo = {
+ first_name: '',
+ last_name: '',
+ address1: '',
+ address2: '',
+ country: '',
+ state: '',
+ zip: '',
+ city: '',
+ last_four: '',
+ card_type: '',
+ month: '',
+ year: '',
+ newBilling: true
+ };
+ this.invoices = [];
+ this.plansError = '';
+ this.unsubscribing = '';
+ this.updatePlan = '';
+ },
+ _clearStore: function() {
+ this.initialize();
+ },
+ _receiveBillingInfo: function(payload) {
+ this.initialize();
+ _.merge(this.billingInfo, payload.billingInfo);
+ _.merge(this.accountInfo, payload.accountInfo);
+ _.merge(this.currentPlan, payload.currentPlan);
+ this.emitChange();
+ },
+ _receiveBillingSubscription: function(payload) {
+ _.merge(this.currentPlan, payload.currentPlan);
+ this.updatePlan = '';
+ this.emitChange();
+ },
+ _receiveInvoices: function(payload) {
+ this.invoices = payload.invoices;
+ this.emitChange();
+ },
+ _resetCurrentPlan: function(payload) {
+ this.currentPlan = payload.currentPlan;
+ this.emitChange();
+ },
+ _unsubscribe: function() { // UNUSED - deprecated
+ this.unsubscribing = 'subscription';
+ this.emitChange();
+ },
+ _unsubscribePackage: function() { // UNUSED - deprecated
+ this.unsubscribing = 'package';
+ this.emitChange();
+ },
+ _unsubscribePlan: function() { // UNUSED - deprecated
+ this.unsubscribing = 'plan';
+ this.emitChange();
+ },
+ _unsubscribeComplete: function() { // UNUSED - deprecated
+ this.unsubscribing = '';
+ this.emitChange();
+ },
+ _updatePlanStart: function(payload) {
+ this.updatePlan = payload;
+ this.emitChange();
+ },
+ _updatePlanErr: function(payload) {
+ this.unsubscribing = '';
+ this.updatePlan = '';
+ this.plansError = payload;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ currentPlan: this.currentPlan,
+ accountInfo: this.accountInfo,
+ billingInfo: this.billingInfo,
+ invoices: this.invoices,
+ plansError: this.plansError,
+ unsubscribing: this.unsubscribing,
+ updatePlan: this.updatePlan
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.currentPlan = state.currentPlan;
+ this.accountInfo = state.accountInfo;
+ this.billingInfo = state.billingInfo;
+ this.invoices = state.invoices;
+ this.plansError = state.plansError;
+ this.unsubscribing = state.unsubscribing;
+ this.updatePlan = state.updatePlan;
+ }
+});
+
+module.exports = BillingPlansStore;
diff --git a/app/scripts/stores/BitbucketLinkStore.js b/app/scripts/stores/BitbucketLinkStore.js
new file mode 100644
index 0000000000..bc3c1ffb3e
--- /dev/null
+++ b/app/scripts/stores/BitbucketLinkStore.js
@@ -0,0 +1,60 @@
+'use strict';
+
+import _ from 'lodash';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('BitbucketLinkStore');
+
+var BitbucketLinkStore = createStore({
+ storeName: 'BitbucketLinkStore',
+ handlers: {
+ RECEIVE_BITBUCKET_AUTH_URL: '_receiveUrl',
+ BITBUCKET_AUTH_URL_ERROR: '_urlError',
+ BITBUCKET_ASSOCIATE_ERROR: '_associateError'
+ },
+ initialize: function() {
+ this.authURL = '';
+ this.error = '';
+ },
+ _associateError: function(body) {
+ debug(body);
+ if (_.has(body, 'detail') && _.isString(body.detail)) {
+ this.error = body.detail;
+ } else {
+ this.error = 'Error linking your account to Bitbucket. Please check that you do not have the same Bitbucket account linked to another Docker Hub account.';
+ }
+ this.emitChange();
+ setTimeout(this._clearError.bind(this), 5000);
+ },
+ _receiveUrl: function(res) {
+ this.authURL = res.bitbucket_authorization_url;
+ this.emitChange();
+ },
+ _urlError: function(err) {
+ debug(err);
+ this.error = 'Error linking your account to bitbucket.';
+ this.emitChange();
+ setTimeout(this._clearError.bind(this), 5000);
+ },
+ _clearError: function() {
+ this.error = '';
+ this.emitChange();
+ },
+ setURL: function(url) {
+ this.authURL = url;
+ },
+ getState: function() {
+ return {
+ authURL: this.authURL,
+ error: this.error
+ };
+ },
+ rehydrate: function(state) {
+ this.authURL = state.authURL;
+ this.error = state.error;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = BitbucketLinkStore;
diff --git a/app/scripts/stores/ChangePasswordStore.js b/app/scripts/stores/ChangePasswordStore.js
new file mode 100644
index 0000000000..f9c210622c
--- /dev/null
+++ b/app/scripts/stores/ChangePasswordStore.js
@@ -0,0 +1,64 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+const debug = require('debug')('stores: ChangePasswordStore');
+
+var ChangePasswordStore = createStore({
+ storeName: 'ChangePasswordStore',
+ handlers: {
+ CHANGE_PASS_UPDATE: '_updateStore',
+ CHANGE_PASS_SUCCESS: '_changePassSuccess',
+ CHANGE_PASS_CLEAR: '_clearStore',
+ RESET_PASSWORD_SUCCESSFUL: '_changePassSuccess',
+ RESET_PASSWORD_ERROR: '_changePassError'
+ },
+ initialize: function() {
+ this.oldpass = '';
+ this.newpass = '';
+ this.confpass = '';
+ this.reset = false;
+ this.hasErr = false;
+ this.err = '';
+ },
+ _updateStore: function(payload) {
+ this.reset = false;
+ this.hasErr = false;
+ this.err = '';
+ this.oldpass = payload.oldpass;
+ this.newpass = payload.newpass;
+ this.confpass = payload.confpass;
+ this.emitChange();
+ },
+ _changePassSuccess: function() {
+ this._clearStore();
+ this.reset = true;
+ this.emitChange();
+ },
+ _changePassError: function(error) {
+ this._clearStore();
+ this.hasErr = true;
+ this.err = error;
+ this.emitChange();
+ },
+ _clearStore: function() {
+ this.oldpass = '';
+ this.newpass = '';
+ this.confpass = '';
+ this.reset = false;
+ this.hasErr = false;
+ this.err = '';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ oldpass: this.oldpass,
+ newpass: this.newpass,
+ confpass: this.confpass,
+ reset: this.reset,
+ hasErr: this.hasErr,
+ err: this.err
+ };
+ }
+
+});
+
+module.exports = ChangePasswordStore;
diff --git a/app/scripts/stores/CloudBillingStore.js b/app/scripts/stores/CloudBillingStore.js
new file mode 100644
index 0000000000..4489827ae7
--- /dev/null
+++ b/app/scripts/stores/CloudBillingStore.js
@@ -0,0 +1,43 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+
+var BillingPlansStore = createStore({
+ storeName: 'CloudBillingStore',
+ handlers: {
+ RESET_CLOUD_BILLING_PLANS: '_clearStore',
+ RECEIVE_CLOUD_BILLING_INFO: '_receiveBillingInfo',
+ LOGOUT: '_clearStore'
+ },
+ initialize: function() {
+ this.currentPlan = {};
+ this.billingInfo = {};
+ this.accountInfo = {};
+ },
+ _clearStore: function() {
+ this.initialize();
+ this.emitChange();
+ },
+ _receiveBillingInfo: function(payload) {
+ this.billingInfo = payload.billingInfo;
+ this.accountInfo = payload.accountInfo;
+ this.currentPlan = payload.currentPlan;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ currentPlan: this.currentPlan,
+ accountInfo: this.accountInfo,
+ billingInfo: this.billingInfo
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.currentPlan = state.currentPlan;
+ this.accountInfo = state.accountInfo;
+ this.billingInfo = state.billingInfo;
+ }
+});
+
+module.exports = BillingPlansStore;
diff --git a/app/scripts/stores/CloudCouponStore.js b/app/scripts/stores/CloudCouponStore.js
new file mode 100644
index 0000000000..c3bb7add7a
--- /dev/null
+++ b/app/scripts/stores/CloudCouponStore.js
@@ -0,0 +1,54 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var CloudCouponStore = createStore({
+ storeName: 'CloudCouponStore',
+ handlers: {
+ RECEIVE_BILLING_INFO: '_clearStore',
+ CLEAR_CLOUD_COUPON: '_clearStore',
+ UPDATE_COUPON_VALUE: '_updateDiscountValue',
+ BILLING_INFO_UPDATE_FIELD_WITH_VALUE: '_updateCouponCode',
+ BILLING_ERRORS: '_updateErrors'
+ },
+ initialize: function() {
+ this.couponCode = '';
+ this.discountValue = 0;
+ this.hasError = false;
+ },
+ _clearStore: function() {
+ this.initialize();
+ this.emitChange();
+ },
+ _updateCouponCode: function({field, fieldKey, fieldValue}) {
+ this.couponCode = fieldKey === 'coupon_code' ? fieldValue : this.couponValue;
+ this.emitChange();
+ },
+ _updateDiscountValue: function(discount) {
+ this.discountValue = discount;
+ this.emitChange();
+ },
+ _updateErrors: function({fieldErrors}) {
+ if (_.has(fieldErrors, 'coupon_code')) {
+ this.hasError = fieldErrors.coupon_code;
+ }
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ couponCode: this.couponCode,
+ discountValue: this.discountValue,
+ hasError: this.hasError
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.couponCode = state.couponCode;
+ this.discountValue = state.discountValue;
+ this.hasError = state.hasError;
+ }
+});
+
+module.exports = CloudCouponStore;
diff --git a/app/scripts/stores/ConvertToOrgStore.js b/app/scripts/stores/ConvertToOrgStore.js
new file mode 100644
index 0000000000..f100be9945
--- /dev/null
+++ b/app/scripts/stores/ConvertToOrgStore.js
@@ -0,0 +1,41 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('stores: ConvertToOrgStore');
+
+export default createStore({
+ storeName: 'ConvertToOrgStore',
+ handlers: {
+ CONVERT_TO_ORG_BAD_REQUEST: '_badRequest',
+ UPDATE_TO_ORG_OWNER: '_updateOwner'
+ },
+ initialize: function() {
+ this.convertError = false;
+ this.error = {};
+ this.newOwner = '';
+ },
+ _badRequest: function(error) {
+ this.convertError = true;
+ this.error = error;
+ this.emitChange();
+ },
+ _updateOwner: function(payload) {
+ this.newOwner = payload.newOwner;
+ this.convertError = false;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ convertError: this.convertError,
+ error: this.error,
+ newOwner: this.newOwner
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.convertError = state.convertError;
+ this.error = state.error;
+ this.newOwner = state.newOwner;
+ }
+});
diff --git a/app/scripts/stores/CreateRepositoryFormStore.js b/app/scripts/stores/CreateRepositoryFormStore.js
new file mode 100644
index 0000000000..e604fa6a50
--- /dev/null
+++ b/app/scripts/stores/CreateRepositoryFormStore.js
@@ -0,0 +1,147 @@
+'use strict';
+
+import _ from 'lodash';
+import { STATUS } from './common/Constants';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('CreateRepositoryFormStore');
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'CreateRepositoryFormStore',
+ handlers: {
+ CREATE_REPO_CLEAR_FORM: 'initialize',
+ CREATE_REPO_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ CREATE_REPO_ATTEMPT_START: '_attemptStart',
+ CREATE_REPO_BAD_REQUEST: '_badRequest',
+ CREATE_REPO_SUCCESS: '_success',
+ CREATE_REPO_FACEPALM: '_facepalm',
+ CREATE_REPO_RECEIVE_NAMESPACES: '_receiveNamespaces'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this.namespaces = [];
+ this.globalFormError = '';
+
+ this.fields = {
+ user: {},
+ namespace: {},
+ name: {},
+ description: {},
+ full_description: {},
+ is_private: {}
+ };
+
+ this.values = {
+ user: '',
+ namespace: '',
+ name: '',
+ description: '',
+ full_description: '',
+ is_private: true
+ };
+ },
+ _receiveNamespaces({
+ namespaces, selectedNamespace
+ }) {
+ debug('receiving namespaces', namespaces, selectedNamespace);
+ /**
+ * namespaces is equivalent to the response in the namespaces API call
+ */
+ this.namespaces = namespaces.namespaces;
+ if(_.includes(namespaces.namespaces, selectedNamespace)) {
+ this.values.namespace = selectedNamespace;
+ } else {
+ this.values.namespace = namespaces.namespaces[0];
+ }
+ this.emitChange();
+ },
+ _facepalm() {
+ // this happens if things are screwed and we can't recover gracefully
+ this.STATUS = STATUS.FACEPALM;
+ this.emitChange();
+ },
+ _clearForm() {
+ this.initialize();
+ this.emitChange();
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}) {
+ debug(fieldKey, fieldValue);
+ this.fields[fieldKey].hasError = false;
+ this.fields[fieldKey].error = '';
+ this.globalFormError = '';
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _attemptStart() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _success() {
+ this.STATUS = STATUS.SUCCESSFUL_SIGNUP;
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ /**
+ * This function expects keys which match the `this.fields` keys
+ * with an array of errors:
+ *
+ * {
+ * orgname: ['this field is required']
+ * }
+ */
+ let shouldEmitChange = false;
+ // So far obj.detail is only returned when there are no more private repo's
+ // We really need to update the response from the api
+ if (obj.detail) {
+ obj.is_private = [obj.detail];
+ }
+
+ // cycle through the possible form fields
+ this.fields = _.mapValues(this.fields, function (errorObject, key) {
+ if(_.has(obj, key)) {
+ shouldEmitChange = true;
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+ /**
+ * __all__ occurs when "Repository with this Name and Namespace already exists."
+ */
+ if(obj.__all__) {
+ this.globalFormError = obj.__all__[0];
+ shouldEmitChange = true;
+ }
+
+ if(shouldEmitChange) {
+ this.emitChange();
+ }
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS,
+ namespaces: this.namespaces,
+ globalFormError: this.globalFormError
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.fields = state.fields;
+ this.values = state.values;
+ this.namespaces = state.namespaces;
+ this.STATUS = state.STATUS;
+ this.globalFormError = state.globalFormError;
+ }
+});
diff --git a/app/scripts/stores/DashboardContribsStore.js b/app/scripts/stores/DashboardContribsStore.js
new file mode 100644
index 0000000000..cbdae5356f
--- /dev/null
+++ b/app/scripts/stores/DashboardContribsStore.js
@@ -0,0 +1,43 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('DashboardContribsStore');
+
+export default createStore({
+ storeName: 'DashboardContribsStore',
+ handlers: {
+ RECEIVE_CONTRIB: '_receiveContribRepos',
+ LOGOUT: 'initialize'
+ },
+ initialize: function() {
+ this.count = 0;
+ this.contribs = [];
+ this.next = null;
+ this.prev = null;
+ },
+ _receiveContribRepos: function(res) {
+ this.count = res.count;
+ this.contribs = res.results;
+ this.next = res.next;
+ this.prev = res.previous;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ count: this.count,
+ contribs: this.contribs,
+ next: this.next,
+ prev: this.prev
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.count = state.count;
+ this.contribs = state.contribs;
+ this.next = state.next;
+ this.prev = state.prev;
+ }
+});
+
diff --git a/app/scripts/stores/DashboardMembersStore.js b/app/scripts/stores/DashboardMembersStore.js
new file mode 100644
index 0000000000..0b2df77622
--- /dev/null
+++ b/app/scripts/stores/DashboardMembersStore.js
@@ -0,0 +1,73 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+var debug = require('debug')('DashboardMembersStore');
+import { STATUS } from './orgteamstore/Constants';
+
+var DashboardMembersStore = createStore({
+ storeName: 'DashboardMembersStore',
+ handlers: {
+ RECEIVE_DASHBOARD_TEAM_MEMBERS: '_receiveDashboardTeamMembers',
+ ORG_DASHBOARD_MEMBERS_ERROR: '_errorReceivingMembers',
+ TEAM_MEMBER_ERROR: '_teamMemberError',
+ TEAM_MEMBER_BAD_REQUEST: '_teamMemberBadRequest',
+ TEAM_MEMBER_UNAUTHORIZED: '_teamMemberUnauthorized',
+ CLEAR_MEMBER_ERROR: '_clearErrorStates'
+ },
+ initialize() {
+ this.members = [];
+ this.count = 0;
+ this.error = {};
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _errorReceivingMembers(err) {
+ debug(err);
+ },
+ _receiveDashboardTeamMembers(members) {
+ debug(members);
+ this.members = members;
+ this.count = members.length;
+ this.emitChange();
+ },
+ _teamMemberError: function(err) {
+ this.STATUS = STATUS.MEMBER_ERROR;
+ this.STATUS = STATUS.GENERAL_SERVER_ERROR;
+ this.error = err;
+ this.emitChange();
+ },
+ _teamMemberBadRequest: function(err) {
+ this.STATUS = STATUS.MEMBER_BAD_REQUEST;
+ this.error = err;
+ this.emitChange();
+ },
+ _teamMemberUnauthorized: function(err) {
+ this.STATUS = STATUS.MEMBER_UNAUTHORIZED;
+ this.error = err;
+ this.emitChange();
+ },
+ _clearErrorStates: function() {
+ this.error = {};
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ members: this.members,
+ count: this.count,
+ error: this.error,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.members = state.members;
+ this.count = state.count;
+ this.STATUS = state.STATUS;
+ this.error = state.error;
+ }
+});
+
+module.exports = DashboardMembersStore;
diff --git a/app/scripts/stores/DashboardNamespacesStore.js b/app/scripts/stores/DashboardNamespacesStore.js
new file mode 100644
index 0000000000..35b354f8b6
--- /dev/null
+++ b/app/scripts/stores/DashboardNamespacesStore.js
@@ -0,0 +1,62 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('DashboardNamespacesStore');
+import _ from 'lodash';
+
+export default createStore({
+ storeName: 'DashboardNamespacesStore',
+ handlers: {
+ RECEIVE_DASHBOARD_NAMESPACES: '_receiveOrgs',
+ CURRENT_USER_CONTEXT: '_setContext',
+ CREATE_REPO_RECEIVE_NAMESPACES: '_receiveOwnedNamespaces'
+ },
+ initialize() {
+ this.namespaces = [];
+ this.currentUserContext = '';
+ this.ownedNamespaces = [];
+ },
+ _receiveOrgs(res) {
+ //There are two API calls possible to get namespaces
+ //`/v2/namespaces` -> returns `res.orgs.namespaces` an object with {namespaces: ['ns1', 'ns2', 'etc']}
+ //`/v2/orgs` -> returns `res.orgs.results` with all orgs the user has read access on. We merge the `res.user` with this list
+ if (res.orgs.namespaces) {
+ this.namespaces = res.orgs.namespaces;
+ } else if (res.orgs.results && _.isArray(res.orgs.results)) {
+ var nsArray = _.pluck(res.orgs.results, 'orgname');
+ nsArray.unshift(res.user);
+ this.namespaces = nsArray;
+ }
+ this.emitChange();
+ },
+ _receiveOwnedNamespaces({
+ namespaces, selectedNamespace
+ }) {
+ debug('receiving namespaces', namespaces, selectedNamespace);
+ /**
+ * namespaces is equivalent to the response in the namespaces API call
+ */
+ this.ownedNamespaces = namespaces.namespaces;
+ this.emitChange();
+ },
+ _setContext: function({username}) {
+ this.currentUserContext = username;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ currentUserContext: this.currentUserContext,
+ namespaces: this.namespaces,
+ ownedNamespaces: this.ownedNamespaces
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.currentUserContext = state.currentUserContext;
+ this.namespaces = state.namespaces;
+ this.ownedNamespaces = state.ownedNamespaces;
+ }
+});
+
diff --git a/app/scripts/stores/DashboardReposStore.js b/app/scripts/stores/DashboardReposStore.js
new file mode 100644
index 0000000000..1501ffc8a4
--- /dev/null
+++ b/app/scripts/stores/DashboardReposStore.js
@@ -0,0 +1,69 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('DashboardReposStore');
+import { STATUS } from './common/Constants';
+const {
+ ATTEMPTING,
+ DEFAULT,
+ SUCCESSFUL
+} = STATUS;
+
+var DashboardReposStore = createStore({
+ storeName: 'DashboardReposStore',
+ handlers: {
+ RECEIVE_REPOS: '_receiveRepos',
+ LOGOUT: 'initialize',
+ DASHBOARD_REPOS_STORE_ATTEMPTING_GET_REPOS: '_startGetRepos',
+ DASHBOARD_REPOS_STORE_ATTEMPTING_GET_ALL_REPOS: '_startGetAllRepos',
+ DASHBOARD_REPOS_STORE_RECEIVE_ALL_REPOS_SUCCESS: '_receiveAllRepos'
+ },
+ initialize() {
+ this.repos = [];
+ this.count = 0;
+ this.next = null;
+ this.prev = null;
+ this.STATUS = DEFAULT;
+ },
+ getState() {
+ return {
+ repos: this.repos,
+ count: this.count,
+ next: this.next,
+ prev: this.prev,
+ STATUS: this.STATUS
+ };
+ },
+ _startGetRepos: function() {
+ this.STATUS = DEFAULT;
+ this.emitChange();
+ },
+ _startGetAllRepos: function() {
+ this.STATUS = ATTEMPTING;
+ this.emitChange();
+ },
+ _receiveRepos(res) {
+ debug(res);
+ this.repos = res.results;
+ this.count = res.count;
+ this.next = res.next;
+ this.prev = res.previous;
+ this.emitChange();
+ },
+ _receiveAllRepos(res) {
+ this.STATUS = SUCCESSFUL;
+ this._receiveRepos(res);
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.repos = state.repos;
+ this.count = state.count;
+ this.next = state.next;
+ this.prev = state.prev;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = DashboardReposStore;
diff --git a/app/scripts/stores/DashboardStarsStore.js b/app/scripts/stores/DashboardStarsStore.js
new file mode 100644
index 0000000000..2e8da00d21
--- /dev/null
+++ b/app/scripts/stores/DashboardStarsStore.js
@@ -0,0 +1,43 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('DashboardStarsStore');
+
+export default createStore({
+ storeName: 'DashboardStarsStore',
+ handlers: {
+ RECEIVE_STARRED: '_receiveStarredRepos',
+ LOGOUT: 'initialize'
+ },
+ initialize: function() {
+ this.count = 0;
+ this.starred = [];
+ this.next = null;
+ this.prev = null;
+ },
+ _receiveStarredRepos: function(res) {
+ this.count = res.count;
+ this.starred = res.results;
+ this.next = res.next;
+ this.prev = res.previous;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ count: this.count,
+ starred: this.starred,
+ next: this.next,
+ prev: this.prev
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.count = state.count;
+ this.starred = state.starred;
+ this.next = state.next;
+ this.prev = state.prev;
+ }
+});
+
diff --git a/app/scripts/stores/DashboardStore.js b/app/scripts/stores/DashboardStore.js
new file mode 100644
index 0000000000..f8702ca5e7
--- /dev/null
+++ b/app/scripts/stores/DashboardStore.js
@@ -0,0 +1,63 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('DashboardStore');
+
+var DashboardStore = createStore({
+ storeName: 'DashboardStore',
+ handlers: {
+ RECEIVE_STARRED: '_receiveStarredRepos',
+ RECEIVE_CONTRIB: '_receiveContribRepos',
+ RECEIVE_ACTIVITY_FEED: '_receiveActivityFeed',
+ LOGOUT: 'initialize'
+ },
+ initialize: function() {
+ this.user = {};
+ this.org = '';
+ this.starred = [];
+ this.contribs = [];
+ this.feed = [];
+ },
+ getInitState: function() {
+ return {
+ starred: [],
+ contribs: [],
+ org: '',
+ feed: [],
+ user: {}
+ };
+ },
+ _receiveStarredRepos: function(repos) {
+ this.starred = repos.results;
+ this.emitChange();
+ },
+ _receiveContribRepos: function(repos) {
+ this.contribs = repos.results;
+ this.emitChange();
+ },
+ _receiveActivityFeed: function(feed) {
+ this.feed = feed;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ starred: this.starred,
+ contribs: this.contribs,
+ org: this.org,
+ feed: this.feed,
+ user: this.user
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.starred = state.starred;
+ this.contribs = state.contribs;
+ this.feed = state.feed;
+ this.org = state.org;
+ this.user = state.user;
+ }
+});
+
+module.exports = DashboardStore;
diff --git a/app/scripts/stores/DashboardTeamsStore.js b/app/scripts/stores/DashboardTeamsStore.js
new file mode 100644
index 0000000000..74d1fb2447
--- /dev/null
+++ b/app/scripts/stores/DashboardTeamsStore.js
@@ -0,0 +1,100 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+import { STATUS } from './orgteamstore/Constants';
+var debug = require('debug')('DashboardTeamsStore');
+
+var DashboardTeamsStore = createStore({
+ storeName: 'DashboardTeamsStore',
+ handlers: {
+ RECEIVE_DASHBOARD_ORG_TEAMS: '_receiveDashboardOrgTeams',
+ TEAM_ERROR: '_orgTeamError',
+ TEAM_BAD_REQUEST: '_teamBadRequest',
+ TEAM_UNAUTHORIZED: '_teamUnauthorized',
+ UPDATE_TEAM_ERROR: '_updateTeamError',
+ UPDATE_TEAM_SUCCESS: '_updateTeamSuccess',
+ TEAM_READ_ONLY: '_isTeamReadOnly'
+ },
+ initialize() {
+ this.teams = [];
+ this.count = 0;
+ this.teamReadOnly = false;
+ this.errorDetails = {detail: ''};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _receiveDashboardOrgTeams(orgTeams) {
+ debug(orgTeams);
+ this.teams = _.sortBy(orgTeams.results, 'name');
+ this.count = orgTeams.count;
+ this.emitChange();
+ },
+ _orgTeamError: function(err) {
+ this.STATUS = STATUS.TEAM_ERROR;
+ this.STATUS = STATUS.GENERAL_SERVER_ERROR;
+ this.errorDetails = {detail: 'Username does not exist or it is invalid.'};
+ this.emitChange();
+ },
+ _teamBadRequest: function(err) {
+ this.STATUS = STATUS.TEAM_BAD_REQUEST;
+ this.errorDetails = err;
+ this.emitChange();
+ },
+ _teamUnauthorized: function(err) {
+ this.STATUS = STATUS.TEAM_UNAUTHORIZED;
+ this.errorDetails = err;
+ this.emitChange();
+ },
+ _isTeamReadOnly: function(flag) {
+ this.teamReadOnly = flag;
+ this.emitChange();
+ },
+ _updateTeamError: function(err) {
+ this.STATUS = STATUS.UPDATE_TEAM_ERROR;
+ if (err.response) {
+ var errResp = err.response;
+ if (errResp.badRequest) {
+ this.errorDetails = err;
+ } else if (errResp.unauthorized || errResp.forbidden) {
+ this.errorDetails = {detail: 'You are not permitted to edit this team.'};
+ } else {
+ this.errorDetails = {detail: 'Error updating team. Check if name is between 3 and 30 characters with no spaces.'};
+ }
+ }
+ this.emitChange();
+ },
+ _updateTeamSuccess: function(err) {
+ this.STATUS = STATUS.UPDATE_TEAM_SUCCESS;
+ this.success = 'Team successfully updated.';
+ setTimeout(this._clearErrorStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearErrorStates: function() {
+ this.errorDetails = {detail: ''};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ teams: this.teams,
+ count: this.count,
+ teamReadOnly: this.teamReadOnly,
+ success: this.success,
+ errorDetails: this.errorDetails
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.teams = state.teams;
+ this.teamReadOnly = state.teamReadOnly;
+ this.count = state.count;
+ this.success = state.success;
+ this.errorDetails = state.errorDetails;
+ }
+});
+
+module.exports = DashboardTeamsStore;
diff --git a/app/scripts/stores/DeletePipelineStore.js b/app/scripts/stores/DeletePipelineStore.js
new file mode 100644
index 0000000000..ef0681321e
--- /dev/null
+++ b/app/scripts/stores/DeletePipelineStore.js
@@ -0,0 +1,46 @@
+'use strict';
+
+var createStore = require('fluxible/addons/createStore');
+import {
+ ATTEMPTING,
+ DEFAULT,
+ FACEPALM,
+ SUCCESSFUL
+} from './deletepipelinestore/Constants';
+
+var debug = require('debug')('SignupStore');
+
+export default createStore({
+ storeName: 'DeletePipelineStore',
+ handlers: {
+ DELETE_PIPELINE_ATTEMPTING: '_start',
+ DELETE_PIPELINE_FACEPALM: '_facepalm',
+ DELETE_PIPELINE_SUCCESS: '_success'
+ },
+ initialize() {
+ this.STATUS = DEFAULT;
+ },
+ _start() {
+ this.STATUS = ATTEMPTING;
+ this.emitChange();
+ },
+ _facepalm() {
+ this.STATUS = FACEPALM;
+ this.emitChange();
+ },
+ _success() {
+ this.STATUS = SUCCESSFUL;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate() {
+ return {};
+ },
+ rehydrate(state) {
+ this.state = state;
+ }
+});
diff --git a/app/scripts/stores/DeleteRepoFormStore.js b/app/scripts/stores/DeleteRepoFormStore.js
new file mode 100644
index 0000000000..21a60d3d33
--- /dev/null
+++ b/app/scripts/stores/DeleteRepoFormStore.js
@@ -0,0 +1,75 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { STATUS } from './deleterepostore/Constants.js';
+const debug = require('debug')('DeleteRepoFormStore');
+
+var DeleteRepoFormStore = createStore({
+ storeName: 'DeleteRepoFormStore',
+ handlers: {
+ DELETE_REPO_ATTEMPT_START: '_deleteRepoAttemptStart',
+ DELETE_REPO_BAD_REQUEST: '_deleteRepoBadRequest',
+ DELETE_REPO_ERROR: '_deleteRepoError',
+ DELETE_REPO_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ RECEIVE_REPOSITORY: '_receiveRepository',
+ TOGGLE_DELETE_REPO_NAME_CONFIRM_BOX: '_toggleConfirmBox'
+ },
+ initialize: function() {
+ this.error = '';
+ this.STATUS = STATUS.DEFAULT;
+ this.values = {
+ confirmRepoName: ''
+ };
+ },
+ _deleteRepoAttemptStart: function() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _deleteRepoBadRequest: function(res) {
+ this.STATUS = STATUS.FORM_ERROR;
+ this.error = res.detail ? res.detail
+ : 'Error deleting repository. Please verify if you have permissions.';
+ this.emitChange();
+ },
+ _deleteRepoError: function() {
+ this.STATUS = STATUS.FORM_ERROR;
+ this.error = 'Error deleting repository. Please verify if you have permissions.';
+ this.emitChange();
+ },
+ _receiveRepository: function() {
+ this.initialize();
+ this.emitChange();
+ },
+ _toggleConfirmBox: function() {
+ if (this.STATUS === STATUS.DEFAULT) {
+ this.STATUS = STATUS.SHOWING_CONFIRM_BOX;
+ } else {
+ this.STATUS = STATUS.DEFAULT;
+ }
+ this.error = '';
+ this.values.confirmRepoName = '';
+ this.emitChange();
+ },
+ _updateFieldWithValue: function({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ this.error = '';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ error: this.error,
+ STATUS: this.STATUS,
+ values: this.values
+ };
+ },
+ rehydrate: function(state) {
+ this.error = state.error;
+ this.STATUS = state.STATUS;
+ this.values = state.values;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = DeleteRepoFormStore;
diff --git a/app/scripts/stores/EmailNotifStore.js b/app/scripts/stores/EmailNotifStore.js
new file mode 100644
index 0000000000..4ada6a99f9
--- /dev/null
+++ b/app/scripts/stores/EmailNotifStore.js
@@ -0,0 +1,128 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+import { STATUS } from './common/Constants';
+var debug = require('debug')('EmailNotifStore:');
+
+
+export default createStore({
+ storeName: 'EmailNotifStore',
+ handlers: {
+ RECEIVE_NOTIFICATIONS: '_receiveNotifications',
+ NOTIF_CHECKBOX_CLICK: '_updateNotifications',
+ RESET_EMAIL_NOTIFICATIONS_STORE: '_resetBlankSlate',
+ SAVE_NOTIFICATIONS_ERROR: '_saveNotifError',
+ SAVE_NOTIFICATIONS_SUCCESS: '_saveNotifSuccess'
+ },
+ initialize: function() {
+ // initialize with data from db
+ this.starNotification = false;
+ this.imgCommentNotification = false;
+ this.autoBuildNotification = false;
+ this.starNotificationID = -1;
+ this.imgCommentNotificationID = -1;
+ this.autoBuildNotificationID = -1;
+ this.STATUS = STATUS.DEFAULT;
+ this.blankNotificationSlate = {};
+ },
+ _receiveNotifications: function(notifications) {
+ for (var i = 0; i < notifications.length; ++i) {
+ switch(notifications[i].notification) {
+ case 'new_repo_comment':
+ this.imgCommentNotification = true;
+ this.imgCommentNotificationID = notifications[i].id;
+ break;
+ case 'new_repo_star':
+ this.starNotification = true;
+ this.starNotificationID = notifications[i].id;
+ break;
+ case 'trusted_build_fail':
+ this.autoBuildNotification = true;
+ this.autoBuildNotificationID = notifications[i].id;
+ break;
+ }
+ }
+ this.blankNotificationSlate = this.getState();
+ debug(this.blankNotificationSlate);
+ this.attempting = false;
+ this.emitChange();
+ },
+ _resetBlankSlate: function() {
+ var slate = this.blankNotificationSlate;
+ debug('RESET EMAIL NOTIF BLANK SLATE');
+ this.starNotification = slate.starNotification;
+ this.imgCommentNotification = slate.imgCommentNotification;
+ this.autoBuildNotification = slate.autoBuildNotification;
+ this.starNotificationID = slate.starNotificationID;
+ this.imgCommentNotificationID = slate.imgCommentNotificationID;
+ this.autoBuildNotificationID = slate.autoBuildNotificationID;
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _updateNotifications: function(cboxType) {
+ this.STATUS = STATUS.DEFAULT;
+ switch(cboxType) {
+ case 'starNotification':
+ this.starNotification = !this.starNotification;
+ break;
+ case 'imgCommentNotification':
+ this.imgCommentNotification = !this.imgCommentNotification;
+ break;
+ case 'autoBuildNotification':
+ this.autoBuildNotification = !this.autoBuildNotification;
+ break;
+ }
+ this.emitChange();
+ },
+ _saveNotifError: function() {
+ this.STATUS = 'ERROR';
+ this.emitChange();
+ },
+ _saveNotifSuccess: function() {
+ this.STATUS = STATUS.SUCCESSFUL;
+ this.emitChange();
+ },
+ hasChanged: function(type) {
+ switch (type) {
+ case 'auto':
+ return (this.autoBuildNotification !== this.blankNotificationSlate.autoBuildNotification);
+ case 'star':
+ return (this.starNotification !== this.blankNotificationSlate.starNotificationID);
+ case 'comment':
+ return (this.imgCommentNotification !== this.blankNotificationSlate.imgCommentNotificationID);
+ default:
+ break;
+ }
+ },
+ getAttempt: function() {
+ return this.attempting;
+ },
+ setAttempt: function(flag) {
+ this.attempting = flag;
+ },
+ getState: function() {
+ return {
+ starNotification: this.starNotification,
+ imgCommentNotification: this.imgCommentNotification,
+ autoBuildNotification: this.autoBuildNotification,
+ starNotificationID: this.starNotificationID,
+ imgCommentNotificationID: this.imgCommentNotificationID,
+ autoBuildNotificationID: this.autoBuildNotificationID,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return _.merge({}, this.getState(), {blankNotificationSlate: this.blankNotificationSlate});
+ },
+ rehydrate: function(state) {
+ this.starNotification = state.starNotification;
+ this.imgCommentNotification = state.imgCommentNotification;
+ this.autoBuildNotification = state.autoBuildNotification;
+ this.starNotificationID = state.starNotificationID;
+ this.imgCommentNotificationID = state.imgCommentNotificationID;
+ this.autoBuildNotificationID = state.autoBuildNotificationID;
+ this.STATUS = state.STATUS;
+ this.blankNotificationSlate = state.blankNotificationSlate;
+ }
+});
diff --git a/app/scripts/stores/EmailsStore.js b/app/scripts/stores/EmailsStore.js
new file mode 100644
index 0000000000..d48589a878
--- /dev/null
+++ b/app/scripts/stores/EmailsStore.js
@@ -0,0 +1,155 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import merge from 'lodash/object/merge';
+import has from 'lodash/object/has';
+import find from 'lodash/collection/find';
+import cloneDeep from 'lodash/lang/cloneDeep';
+import { STATUS, EMAILSTATUS } from './emailsstore/Constants';
+
+var debug = require('debug')('EmailsStore');
+
+var EmailsStore = createStore({
+ storeName: 'EmailsStore',
+ handlers: {
+ RECEIVE_EMAILS: '_receiveEmails',
+ CHANGE_ROUTE: '_resetState',
+ ADD_EMAIL_INVALID: '_addEmailInvalid',
+ ADD_EMAIL_SUCCESS: '_addEmailSuccess',
+ START_SAVE_ACTION: '_startSaveAction',
+ FINISH_SAVE_ACTION: '_finishSaveAction',
+ RESEND_EMAIL_CONFIRMATION_ATTEMPT_START: '_resendEmailConfirmationAttemptStart',
+ RESEND_EMAIL_CONFIRMATION_SENT: '_resendEmailConfirmationSent',
+ RESEND_EMAIL_CONFIRMATION_FAILED: '_resendEmailConfirmationFail',
+ RESEND_EMAIL_CONFIRMATION_CLEAR: '_resendClear',
+ UPDATE_ADD_EMAIL: '_updateAddEmail'
+ },
+ initialize: function() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this._cleanSlate = {
+ emails: []
+ };
+ this.emails = [];
+ this.emailConfirmations = {};
+ this.addEmail = '';
+ this.addError = '';
+ },
+ _startSaveAction() {
+ this.STATUS = STATUS.SAVING;
+ this.emitChange();
+ },
+ _finishSaveAction() {
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _resendEmailConfirmationAttemptStart(emailID) {
+ debug('RESEND CONFIRMATION START');
+ this.emailConfirmations = merge(this.emailConfirmations, {
+ [emailID]: EMAILSTATUS.ATTEMPTING
+ });
+ this.emitChange();
+ },
+ _resendEmailConfirmationSent(emailID) {
+ debug('RESEND CONFIRMATION SENT');
+ this.emailConfirmations = merge(this.emailConfirmations, {
+ [emailID]: EMAILSTATUS.SUCCESS
+ });
+ this.emitChange();
+ },
+ _resendEmailConfirmationFail(emailID) {
+ debug('RESEND CONFIRMATION FAIL');
+ this.emailConfirmations = merge(this.emailConfirmations, {
+ [emailID]: EMAILSTATUS.FAILED
+ });
+ this.emitChange();
+ },
+ _resendClear(emailID) {
+ debug('RESEND CONFIRMATION CLEAR');
+ this.emailConfirmations = merge(this.emailConfirmations, {
+ [emailID]: ''
+ });
+ this.emitChange();
+ },
+ _receiveEmails: function(payload) {
+ debug(payload);
+ this.initialize();
+ this._cleanSlate = {
+ /**
+ * We cloneDeep here because otherwise this.emails
+ * and this._cleanSlate.emails will refer to the
+ * same array causing unintuitive behavior.
+ */
+ emails: cloneDeep(payload.emails)
+ };
+ this.emails = payload.emails;
+ this.emitChange();
+ },
+ _addEmailInvalid(error) {
+ this.addError = error[0];
+ this.emitChange();
+ },
+ _addEmailSuccess() {
+ this.addEmail = '';
+ this.emitChange();
+ },
+ _resetState() {
+ var {emails} = this._cleanSlate;
+ this.emails = emails.slice();
+ this.addError = '';
+ this.emitChange();
+ },
+ _updateAddEmail(email) {
+ this.addEmail = email;
+ this.emitChange();
+ },
+ isCleanSlatePrimaryEmail: function(email: string) {
+ debug('cleanSlate.emails', this._cleanSlate.emails, email);
+ /**
+ * A function that answers "is this email address a primary email
+ * address?" with respect to the database, not with respect
+ * to the state of the client side application
+ */
+ var primaryEmail = find(this._cleanSlate.emails, function(obj) {
+ return obj.email === email && obj.primary === true;
+ });
+
+ debug('primaryEmail', !!primaryEmail);
+ return !!primaryEmail;
+ },
+ getCleanSlatePrimaryEmailID() {
+ return find(this._cleanSlate.emails, function(obj) {
+ return obj.primary === true;
+ }).id;
+ },
+ getEmails: function() {
+ return {
+ emails: this.emails
+ };
+ },
+ getState: function() {
+ return {
+ STATUS: this.STATUS,
+ emails: this.emails,
+ addEmail: this.addEmail,
+ addError: this.addError,
+ emailConfirmations: this.emailConfirmations
+ };
+ },
+ dehydrate() {
+ return merge({},
+ this.getState(),
+ {
+ _cleanSlate: this._cleanSlate
+ });
+ },
+ rehydrate(state) {
+ this._cleanSlate = state._cleanSlate;
+ this.addEmail = state.addEmail;
+ this.emails = state.emails.slice(0);
+ this.emailConfirmations = state.emailConfirmations;
+ this.addError = state.addError;
+ }
+});
+
+module.exports = EmailsStore;
diff --git a/app/scripts/stores/EnterprisePaidFormStore.js b/app/scripts/stores/EnterprisePaidFormStore.js
new file mode 100644
index 0000000000..f77def90e6
--- /dev/null
+++ b/app/scripts/stores/EnterprisePaidFormStore.js
@@ -0,0 +1,294 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { STATUS } from './common/Constants';
+var debug = require('debug')('EnterprisePaidFormStore');
+import each from 'lodash/collection/each';
+import includes from 'lodash/collection/includes';
+import merge from 'lodash/object/merge';
+import keys from 'lodash/object/keys';
+import has from 'lodash/object/has';
+import mapValues from 'lodash/object/mapValues';
+import isString from 'lodash/lang/isString';
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'EnterprisePaidFormStore',
+ handlers: {
+ ENTERPRISE_PAID_RECEIVE_ORGS: '_receiveOrgs',
+ ENTERPRISE_PAID_CLEAR_FORM: '_clearStore',
+ ENTERPRISE_PAID_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+
+ ENTERPRISE_PAID_ATTEMPT_START: '_enterprisePaidAttemptStart',
+ ENTERPRISE_PAID_BAD_REQUEST: '_badRequest',
+ ENTERPRISE_PAID_SUCCESS: '_signupSuccess',
+ ENTERPRISE_PAID_FACEPALM: '_facepalm',
+
+ BILLING_SUBMIT_START: '_enterprisePaidAttemptStart',
+ BILLING_SUBMIT_SUCCESS: '_signupSuccess',
+ BILLING_SUBMIT_ERROR: '_badRequest',
+
+ GET_RECURLY_ERROR: '_updateRecurlyErrors',
+ ENTERPRISE_PAID_GET_RECURLY_ERROR: 'recurlyError',
+ ENTERPRISE_PAID_API_ERROR: '_apiError',
+ ENTERPRISE_PAID_ERRORS: '_validateErrors',
+ ENTERPRISE_PAID_POPULATE_FORM: '_populateForm'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this.globalFormError = '';
+ this.orgs = [];
+
+ this.fields = {
+ first_name: {},
+ last_name: {},
+ postal_code: {},
+ number: {},
+ month: {},
+ year: {},
+ cvv: {},
+ address1: {},
+ city: {},
+ state: {},
+ country: {},
+ expiry: {},
+ email: {}
+ };
+
+ this.values = {
+ first_name: '',
+ last_name: '',
+ postal_code: '',
+ number: '',
+ month: '01',
+ year: '2015',
+ cvv: '',
+ address1: '',
+ city: '',
+ state: '',
+ country: 'US',
+ last_four: '',
+ card_type: '',
+ account_first: '',
+ account_last: '',
+ company_name: '',
+ email: ''
+ };
+ },
+ _clearStore(){
+ this.initialize();
+ this.emitChange();
+ },
+ _populateForm({
+ first_name,
+ last_name,
+ zip,
+ month,
+ year,
+ address1,
+ address2,
+ city,
+ state,
+ country,
+ last_four,
+ card_type,
+ account_first,
+ account_last,
+ company_name,
+ email
+ }) {
+ var D = new Date();
+ var defaultMonth = D.getMonth();
+ var defaultYear = D.getFullYear() + 1;
+ var defaultCountry = 'US';
+ this.fields = {
+ first_name: {},
+ last_name: {},
+ postal_code: {},
+ number: {},
+ month: {},
+ year: {},
+ cvv: {},
+ address1: {},
+ address2: {},
+ city: {},
+ state: {},
+ country: {},
+ expiry: {},
+ email: {}
+ };
+ this.values = {
+ first_name,
+ last_name,
+ postal_code: zip,
+ month: month || defaultMonth,
+ year: year || defaultYear,
+ address1,
+ address2,
+ city,
+ state,
+ country: country || defaultCountry,
+ last_four,
+ card_type,
+ account_first,
+ account_last,
+ company_name,
+ email
+ };
+ this.STATUS = STATUS.DEFAULT;
+ this.globalFormError = '';
+ this.emitChange();
+ },
+ _updateRecurlyErrors(error) {
+ const errorFields = error.fields;
+ debug('Recurly Form errors', errorFields);
+ let fieldErrors = {
+ number: {
+ hasError: includes(errorFields, 'number'),
+ error: 'There was an error processing your card'
+ },
+ expiry: {
+ hasError: includes(errorFields, 'month') || includes(errorFields, 'year'),
+ error: 'This field is invalid'
+ },
+ cvv: {
+ hasError: includes(errorFields, 'cvv'),
+ error: 'This field is invalid'
+ },
+ first_name: {
+ hasError: includes(errorFields, 'first_name'),
+ error: 'This field is required'
+ },
+ last_name: {
+ hasError: includes(errorFields, 'last_name'),
+ error: 'This field is required'
+ },
+ postal_code: {
+ hasError: includes(errorFields, 'postal_code'),
+ error: 'This field is invalid'
+ }
+ };
+ merge(this.fields, fieldErrors);
+ this.STATUS = STATUS.DEFAULT;
+ this.globalFormError = error.message;
+ this.emitChange();
+ },
+ _facepalm() {
+ // this happens if things are screwed and we can't recover gracefully
+ this.STATUS = STATUS.FACEPALM;
+ this.emitChange();
+ },
+ recurlyError(fields) {
+ this.STATUS = STATUS.DEFAULT;
+ var emitChange = false;
+ each(fields, function(val, idx) {
+ if(includes(keys(this.fields), val)) {
+ emitChange = true;
+ this.fields[val] = {
+ hasError: true,
+ error: 'This field is required'
+ };
+ }
+ }, this);
+
+ if(emitChange) {
+ this.emitChange();
+ }
+ },
+ _validateErrors(hasError) {
+ each(hasError, (v, k) => {
+ if (v) {
+ if (includes(['number', 'cvv', 'month', 'year', 'country'], k)) {
+ this.fields[k] = {
+ hasError: true,
+ error: 'Invalid ' + k
+ };
+ } else {
+ this.fields[k] = {
+ hasError: true,
+ error: 'Required'
+ };
+ }
+ }
+ });
+ this.emitChange();
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}) {
+ debug(fieldKey, fieldValue);
+ this.fields[fieldKey] = {hasError: false, error: ''};
+ if (includes(['month', 'year'], fieldKey)) {
+ this.fields.expiry = {hasError: false, error: ''};
+ }
+ if (fieldKey === 'number') {
+ let card_type = window.recurly.validate.cardType(fieldValue);
+ this.values.card_type = card_type;
+ }
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _signupSuccess() {
+ this.STATUS = STATUS.SUCCESSFUL;
+ this.emitChange();
+ },
+ _receiveOrgs(namespaces) {
+ this.orgs = namespaces;
+ this.emitChange();
+ },
+ _apiError() {
+ this.STATUS = STATUS.FACEPALM;
+ this.emitChange();
+ },
+ _enterprisePaidAttemptStart() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ this.STATUS = STATUS.ERROR;
+
+ // cycle through the possible form fields
+ this.fields = mapValues(this.fields, function (errorObject, key) {
+ if(has(obj, key)) {
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+
+ if(has(obj, 'non_field_errors')) {
+ this.globalFormError = obj.non_field_errors[0];
+ } else if (has(obj, 'detail')) {
+ this.globalFormError = obj.detail;
+ } else if (isString(obj)) {
+ this.globalFormError = obj;
+ }
+
+ this.emitChange();
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS,
+ orgs: this.orgs,
+ globalFormError: this.globalFormError
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.orgs = state.orgs;
+ this.values = state.values;
+ this.fields = state.fields;
+ this.STATUS = state.STATUS;
+ this.globalFormError = this.globalFormError;
+ }
+});
diff --git a/app/scripts/stores/EnterprisePartnerTrackingStore.js b/app/scripts/stores/EnterprisePartnerTrackingStore.js
new file mode 100644
index 0000000000..6c5afaea8b
--- /dev/null
+++ b/app/scripts/stores/EnterprisePartnerTrackingStore.js
@@ -0,0 +1,29 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('EnterprisePartnerTrackingStore');
+
+export default createStore({
+ storeName: 'EnterprisePartnerTrackingStore',
+ handlers: {
+ ENTERPRISE_PARTNER_RECEIVE_CODE: '_receivePartnerTrackingCode'
+ },
+ initialize() {
+ this.partnervalue = '';
+ },
+ _receivePartnerTrackingCode({ code }) {
+ this.partnervalue = code;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ partnervalue: this.partnervalue
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.partnervalue = state.partnervalue;
+ }
+});
diff --git a/app/scripts/stores/EnterpriseTrialFormStore.js b/app/scripts/stores/EnterpriseTrialFormStore.js
new file mode 100644
index 0000000000..e967f84876
--- /dev/null
+++ b/app/scripts/stores/EnterpriseTrialFormStore.js
@@ -0,0 +1,140 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { ATTEMPTING, DEFAULT, FACEPALM, SUCCESSFUL_SIGNUP } from 'stores/enterprisetrialstore/Constants';
+var debug = require('debug')('EnterpriseTrialFormStore');
+var _ = require('lodash');
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'EnterpriseTrialFormStore',
+ handlers: {
+ ENTERPRISE_TRIAL_RECEIVE_ORGS: '_receiveOrgs',
+ ENTERPRISE_TRIAL_CLEAR_FORM: '_clearForm',
+ ENTERPRISE_TRIAL_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ ENTERPRISE_TRIAL_ATTEMPT_START: '_attemptStart',
+ ENTERPRISE_TRIAL_BAD_REQUEST: '_badRequest',
+ ENTERPRISE_TRIAL_SUCCESS: '_signupSuccess',
+ ENTERPRISE_TRIAL_FACEPALM: '_facepalm',
+ CREATED_ORGANIZATION: '_clearForm'
+ },
+ initialize() {
+ this.STATUS = DEFAULT;
+ this.orgs = [];
+ this.globalFormError = '';
+
+ this.fields = {
+ firstName: {},
+ lastName: {},
+ companyName: {},
+ jobFunction: {},
+ email: {},
+ phoneNumber: {},
+ country: {},
+ state: {},
+ namespace: {}
+ };
+
+ this.values = {
+ namespace: '',
+ firstName: '',
+ lastName: '',
+ jobFunction: '',
+ companyName: '',
+ email: '',
+ phoneNumber: '',
+ country: 'US',
+ state: ''
+ };
+ },
+ _facepalm() {
+ // this happens if things are screwed and we can't recover gracefully
+ this.STATUS = FACEPALM;
+ this.globalFormError = 'Something went wrong on the server. We have been alerted to this issue';
+ this.emitChange();
+ },
+ _clearForm() {
+ this.initialize();
+ this.emitChange();
+ },
+ _clearErrors() {
+ this.fields = {
+ firstName: {},
+ lastName: {},
+ jobFunction: {},
+ companyName: {},
+ email: {},
+ phoneNumber: {},
+ country: {},
+ state: {},
+ namespace: {}
+ };
+ this.globalFormError = '';
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}) {
+ this.STATUS = DEFAULT;
+ this.globalFormError = '';
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _attemptStart() {
+ this.STATUS = ATTEMPTING;
+ this.emitChange();
+ },
+ _signupSuccess() {
+ this.STATUS = SUCCESSFUL_SIGNUP;
+ this._clearErrors();
+ this.emitChange();
+ },
+ _receiveOrgs(namespaces) {
+ this.orgs = namespaces;
+ //will always have at least current logged in namespace
+ this.values.namespace = namespaces[0];
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ this._clearErrors();
+ this.STATUS = DEFAULT;
+
+ // cycle through the possible form fields
+ this.fields = _.mapValues(this.fields, (errorObject, key) => {
+ if(_.has(obj, key)) {
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+
+ if(obj && obj.non_field_errors) {
+ this.globalFormError = obj.non_field_errors[0];
+ }
+ this.emitChange();
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS,
+ orgs: this.orgs,
+ globalFormError: this.globalFormError
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.orgs = state.orgs;
+ this.fields = state.fields;
+ this.values = state.values;
+ this.STATUS = state.STATUS;
+ this.globalFormError = state.globalFormError;
+
+ }
+});
diff --git a/app/scripts/stores/EnterpriseTrialSuccessStore.js b/app/scripts/stores/EnterpriseTrialSuccessStore.js
new file mode 100644
index 0000000000..b68cf015af
--- /dev/null
+++ b/app/scripts/stores/EnterpriseTrialSuccessStore.js
@@ -0,0 +1,47 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { DEFAULT,
+ ERROR } from 'stores/enterprisetrialsuccessstore/Constants';
+const debug = require('debug')('EnterpriseTrialSuccessStore');
+
+export default createStore({
+ storeName: 'EnterpriseTrialSuccessStore',
+ handlers: {
+ RECEIVE_TRIAL_LICENSE_FACEPALM: '_facepalm',
+ RECEIVE_TRIAL_LICENSE: '_receiveTrialLicense',
+ RECEIVE_TRIAL_LICENSE_BAD_REQUEST: '_receiveTrialLicenseBadRequest'
+ },
+ initialize: function() {
+ this.license = {};
+ this.STATUS = DEFAULT;
+ },
+ _facepalm: function(err) {
+ this.STATUS = ERROR;
+ debug(err);
+ this.emitChange();
+ },
+ _receiveTrialLicense: function(license) {
+ this.license = license;
+ this.emitChange();
+ },
+ _receiveTrialLicenseBadRequest: function(err) {
+ this.STATUS = ERROR;
+ debug(err);
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ license: this.license,
+ STATUS: this.STATUS
+ };
+ },
+ rehydrate: function(state) {
+ this.license = state.license;
+ this.STATUS = state.STATUS;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
diff --git a/app/scripts/stores/GithubLinkStore.js b/app/scripts/stores/GithubLinkStore.js
new file mode 100644
index 0000000000..212e90a594
--- /dev/null
+++ b/app/scripts/stores/GithubLinkStore.js
@@ -0,0 +1,61 @@
+'use strict';
+
+import _ from 'lodash';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('GithubLinkStore');
+
+var GithubLinkStore = createStore({
+ storeName: 'GithubLinkStore',
+ handlers: {
+ RECEIVE_GITHUB_ID: '_receiveID',
+ GITHUB_ID_ERROR: '_idError',
+ GITHUB_SECURITY_ERROR: '_githubSecurityError',
+ GITHUB_ASSOCIATE_ERROR: '_githubAssociateError'
+ },
+ initialize: function() {
+ this.githubClientID = '';
+ this.error = '';
+ },
+ _receiveID: function(res) {
+ this.githubClientID = res.client_id;
+ this.emitChange();
+ },
+ _idError: function(err) {
+ debug(err);
+ },
+ _githubAssociateError: function(body) {
+ debug(body);
+ if (_.has(body, 'detail') && _.isString(body.detail)) {
+ this.error = body.detail;
+ } else {
+ this.error = 'There was an error during the Github account link. Please check that you do not have the same Github account linked to another Docker Hub account.';
+ }
+ this.emitChange();
+ setTimeout(this._clearError.bind(this), 5000);
+ },
+ _githubSecurityError: function(errorState) {
+ debug(errorState);
+ this.error = 'There was a security error during the github account linking process.';
+ this.emitChange();
+ setTimeout(this._clearError.bind(this), 5000);
+ },
+ _clearError: function() {
+ this.error = '';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ githubClientID: this.githubClientID,
+ error: this.error
+ };
+ },
+ rehydrate: function(state) {
+ this.githubClientID = state.githubClientID;
+ this.error = state.error;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = GithubLinkStore;
diff --git a/app/scripts/stores/JWTStore.js b/app/scripts/stores/JWTStore.js
new file mode 100644
index 0000000000..d7fcd3b368
--- /dev/null
+++ b/app/scripts/stores/JWTStore.js
@@ -0,0 +1,73 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('stores: JWTStore');
+import cookie from 'cookie';
+
+export default createStore({
+ storeName: 'JWTStore',
+ handlers: {
+ RECEIVE_JWT: '_receiveJWT',
+ LOGOUT: '_logout',
+ LOGOUT_ERROR: '_logoutError',
+ EXPIRED_SIGNATURE: '_setExpiredSignature'
+ },
+ _receiveJWT(jwt) {
+ this.jwt = jwt;
+ this.signatureIsExpired = false;
+ this.emitChange();
+ },
+ _logoutError(err) {
+ debug(err + ' Logout did not complete cleanly on the server');
+ this._logout(); //we logout on the client side anyway
+ },
+ _logout() {
+ this.jwt = null;
+ this.emitChange();
+ },
+ _logoutWithNotification(){
+ debug('Logging out due to invalid Signature');
+ this._logout();
+ },
+ _setExpiredSignature(){
+ this.signatureIsExpired = true;
+ this.jwt = null;
+ this.emitChange();
+ },
+ getJWT() {
+ return this.jwt;
+ },
+ getState() {
+ return {
+ jwt: this.jwt,
+ signatureIsExpired: this.signatureIsExpired
+ };
+ },
+ isLoggedIn() {
+ //Return true if user is logged in
+ return !!this.jwt;
+ },
+ dehydrate() {
+ if(this.signatureIsExpired) {
+ return {
+ jwt: null,
+ signatureIsExpired: true
+ };
+ } else {
+ return {
+ jwt: this.jwt,
+ signatureIsExpired: false
+ };
+ }
+ },
+ rehydrate(state) {
+ debug('rehydrate', state);
+ if(state.signatureIsExpired) {
+ debug('signatureIsExpired');
+ this._logoutWithNotification();
+ } else {
+ debug('signatureIsValid');
+ this.signatureIsExpired = state.signatureIsExpired;
+ this._receiveJWT(state.jwt);
+ }
+ }
+});
diff --git a/app/scripts/stores/LoginStore.js b/app/scripts/stores/LoginStore.js
new file mode 100644
index 0000000000..a074541f06
--- /dev/null
+++ b/app/scripts/stores/LoginStore.js
@@ -0,0 +1,108 @@
+'use strict';
+
+import _ from 'lodash';
+import { STATUS } from './loginstore/Constants';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('LoginStore');
+
+export default createStore({
+ storeName: 'LoginStore',
+ handlers: {
+ LOGIN_ATTEMPT_START: '_loginAttemptStart',
+ LOGIN_UNAUTHORIZED: '_loginUnauthorized',
+ LOGIN_UNAUTHORIZED_DETAIL: '_loginUnauthorizedDetail',
+ LOGIN_BAD_REQUEST: '_badRequest',
+ LOGIN_ERROR: '_loginError',
+ LOGIN_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ LOGIN_CLEAR: '_clearLoginForm'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+ this.globalFormError = '';
+
+ this.fields = {
+ username: {},
+ password: {}
+ };
+
+ this.values = {
+ username: '',
+ password: ''
+ };
+ },
+ _clearLoginForm() {
+ debug('Clearing');
+ this.initialize();
+ this.emitChange();
+ },
+ _loginAttemptStart() {
+ this.STATUS = STATUS.ATTEMPTING_LOGIN;
+ this.emitChange();
+ },
+ _loginError(err){
+ this.STATUS = STATUS.GENERIC_ERROR;
+ this.globalFormError = 'There was an error contacting the server. Please try again later.';
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ this.STATUS = STATUS.DEFAULT;
+ /**
+ * This function expects keys which match the `this.fields` keys
+ * with an array of errors:
+ *
+ * {
+ * username: ['this field is required']
+ * }
+ */
+ let shouldEmitChange = false;
+
+ // cycle through the possible form fields
+ this.fields = _.mapValues(this.fields, function (errorObject, key) {
+ if(_.has(obj, key)) {
+ shouldEmitChange = true;
+ return {
+ hasError: !!obj[key],
+ error: obj[key][0]
+ };
+ } else {
+ return errorObject;
+ }
+ });
+
+ if(shouldEmitChange) {
+ this.emitChange();
+ }
+ },
+ _loginUnauthorized() {
+ this.STATUS = STATUS.ERROR_UNAUTHORIZED;
+ this.globalFormError = 'Login Failed. The username or password may be incorrect.';
+ this.emitChange();
+ },
+ _loginUnauthorizedDetail({detail}) {
+ this.STATUS = STATUS.ERROR_UNAUTHORIZED;
+ this.globalFormError = detail;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS,
+ globalFormError: this.globalFormError
+ };
+ },
+ _updateFieldWithValue: function({fieldKey, fieldValue}){
+ this.fields[fieldKey] = {
+ hasError: false,
+ error: ''
+ };
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ dehydrate: function() {
+ return {};
+ },
+ rehydrate: function(state) {
+ this.state = state;
+ }
+});
diff --git a/app/scripts/stores/NotifyStore.js b/app/scripts/stores/NotifyStore.js
new file mode 100644
index 0000000000..57c2cc30b5
--- /dev/null
+++ b/app/scripts/stores/NotifyStore.js
@@ -0,0 +1,44 @@
+'use strict';
+
+/**
+ * displays alert-style dismissable notifications to the user
+ */
+
+const createStore = require('fluxible/addons/createStore');
+const debug = require('debug')('NotifyStore');
+
+var NotifyStore = createStore({
+ storeName: 'NotifyStore',
+ handlers: {
+ NEW_ALERT: '_newAlert',
+ EXPIRE_ALERT: '_expireAlert',
+ EXPIRED_SIGNATURE: '_newDetailAlert'
+ },
+ initialize: function() {
+ // alerts have a timestamp-based key
+ this.alerts = {};
+ },
+ _newAlert: function(obj) {
+ this.alerts[+new Date()] = obj.msg;
+ debug(this.alerts);
+ this.emitChange();
+ },
+ _newDetailAlert: function(msg) {
+ debug(msg);
+ this._newAlert({
+ msg: 'You have been logged out because your token has expired or was invalid'
+ });
+ },
+ getState: function() {
+ return this.state;
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ debug(state);
+ this.alerts = state.alerts;
+ }
+});
+
+module.exports = NotifyStore;
diff --git a/app/scripts/stores/OrgTeamStore.js b/app/scripts/stores/OrgTeamStore.js
new file mode 100644
index 0000000000..cc8b54ed7f
--- /dev/null
+++ b/app/scripts/stores/OrgTeamStore.js
@@ -0,0 +1,90 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { STATUS } from './orgteamstore/Constants';
+
+var OrgTeamStore = createStore({
+ storeName: 'OrgTeamStore',
+ handlers: {
+ CREATE_ORG_TEAM: '_createOrgTeam',
+ RECEIVE_ORG_TEAM: '_receiveOrgTeam',
+ RECEIVE_TEAM_MEMBERS: '_receiveOrgMembers',
+ TEAM_ERROR: '_orgTeamError',
+ TEAM_BAD_REQUEST: '_teamBadRequest',
+ TEAM_UNAUTHORIZED: '_teamUnauthorized',
+ ORG_TEAM_CLEAR_ERROR_STATES: '_clearErrorStates'
+ },
+ initialize: function() {
+ // initialize
+ this.name = '';
+ this.description = '';
+ this.members = [];
+ this.errorDetails = {};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ },
+ //TODO: this will be removed once we have API
+ _createOrgTeam: function(payload) {
+ this.name = payload.name;
+ this.description = payload.description;
+ this.STATUS = STATUS.CREATE_TEAM_SUCCESS;
+ this.emitChange();
+ },
+ _receiveOrgTeam: function(orgTeam) {
+ this.name = orgTeam.name;
+ this.description = orgTeam.description;
+ this.emitChange();
+ },
+ _receiveOrgMembers: function(members) {
+ this.members = members;
+ this.emitChange();
+ },
+ _orgTeamError: function(err) {
+ this.STATUS = STATUS.TEAM_ERROR;
+ this.STATUS = STATUS.GENERAL_SERVER_ERROR;
+ this.errorDetails = {detail: 'Error updating team. Check if name is between 3 and 30 characters with no spaces.'};
+ this.emitChange();
+ },
+ _teamBadRequest: function(err) {
+ this.STATUS = STATUS.TEAM_BAD_REQUEST;
+ this.errorDetails = {detail: 'Please check your input values. The team name may already exist or the characters may be invalid.'};
+ this.emitChange();
+ },
+ _teamUnauthorized: function(err) {
+ this.STATUS = STATUS.TEAM_UNAUTHORIZED;
+ this.errorDetails = {detail: 'You have no permission to edit this team.'};
+ this.emitChange();
+ },
+ _clearErrorStates: function() {
+ this.errorDetails = {};
+ this.success = '';
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ name: this.name,
+ description: this.description,
+ members: this.members,
+ errorDetails: this.errorDetails,
+ success: this.success,
+ STATUS: this.STATUS
+ };
+ },
+ getMembers: function() {
+ return this.members;
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.name = state.name;
+ this.description = state.description;
+ this.members = state.members;
+ this.errorDetails = state.errorDetails;
+ this.success = state.success;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = OrgTeamStore;
diff --git a/app/scripts/stores/OrganizationStore.js b/app/scripts/stores/OrganizationStore.js
new file mode 100644
index 0000000000..9b12f12b70
--- /dev/null
+++ b/app/scripts/stores/OrganizationStore.js
@@ -0,0 +1,130 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+
+var OrganizationStore = createStore({
+ storeName: 'OrganizationStore',
+ handlers: {
+ RECEIVE_ORGANIZATION: '_updateOrg',
+ CREATED_ORGANIZATION: '_onCreateOrg',
+ SELECT_ORGANIZATION: '_onCurrentOrgChange',
+ RECEIVE_ORG_TEAMS: '_receiveOrgTeams',
+ CURRENT_USER_ORGS: '_onGetCurrentOrgs',
+ UPDATE_ORG_SUCCESS: '_updateOrgSuccess',
+ UPDATE_ORG_ERROR: '_updateOrgError'
+ },
+ initialize: function() {
+ // initialize
+ this.name = '';
+ this.gravatarURL = 'https://secure.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y';
+ this.currentOrg = {};
+ this.orgs = [];
+ this.currentOrgTeams = [];
+ this.success = '';
+ this.error = '';
+ },
+ _onCreateOrg: function(payload) {
+ this.receiveState({
+ currentOrg: payload.newOrg,
+ orgs: payload.userOrgs
+ });
+ this.emitChange();
+ },
+ _updateOrg: function(payload) {
+ this.receiveState({
+ currentOrg: payload
+ });
+ this.emitChange();
+ },
+ _onGetCurrentOrgs: function(payload) {
+ this.receiveState({
+ orgs: payload.results
+ });
+ this.emitChange();
+ },
+ _onCurrentOrgChange: function(payload) {
+ this.receiveState({
+ currentOrg: this.getOrg(payload)
+ });
+ this.emitChange();
+ },
+ _receiveOrgTeams: function(orgTeams) {
+ this.receiveState({
+ currentOrgTeams: orgTeams.results
+ });
+ this.emitChange();
+ },
+ _updateOrgSuccess: function() {
+ this.success = 'Updated Organization Details Successfully!';
+ setTimeout(this._clearOrgErrorStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _updateOrgError: function(err) {
+ var errResponse = err.response;
+ if (errResponse.badRequest) {
+ _.forIn(errResponse.body, function(v, k) {
+ this.error += k + ': ' + v.join(',') + '\n';
+ }.bind(this));
+ } else if(errResponse.unauthorized || errResponse.forbidden) {
+ this.error = 'You have no permission to edit this organization.';
+ } else {
+ this.error = 'An error occurred during the organization update. Please try again later';
+ }
+ setTimeout(this._clearOrgErrorStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearOrgErrorStates: function() {
+ this.success = '';
+ this.error = '';
+ this.emitChange();
+ },
+ receiveState: function(payload) {
+ this.name = payload.orgname || this.name;
+ this.gravatarURL = payload.gravatar_url || this.gravatarURL;
+ this.currentOrg = payload.currentOrg || this.currentOrg;
+ this.currentOrgTeams = payload.currentOrgTeams || this.currentOrgTeams;
+ this.orgs = payload.orgs || this.orgs;
+ },
+ getState: function() {
+ return {
+ name: this.name,
+ gravatarURL: this.gravatarURL,
+ currentOrg: this.currentOrg,
+ currentOrgTeams: this.currentOrgTeams,
+ orgs: this.orgs,
+ error: this.error,
+ success: this.success
+ };
+ },
+ getOrgs: function() {
+ return this.orgs;
+ },
+ getCurrentOrg: function() {
+ //returns currently selected org
+ return this.currentOrg;
+ },
+ getOrg: function(name) {
+ //Assuming org names are unique and expecting filter to return an array of exactly 1 item
+ return _.filter(this.orgs, function(org) {
+ return org.orgname === name;
+ })[0];
+ },
+ getOrgTeams: function() {
+ return this.currentOrgTeams;
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.name = state.name;
+ this.gravatarURL = state.gravatarURL;
+ this.currentOrg = state.currentOrg;
+ this.currentOrgTeams = state.currentOrgTeams;
+ this.orgs = state.orgs;
+ this.error = state.error;
+ this.success = state.success;
+ }
+});
+
+module.exports = OrganizationStore;
diff --git a/app/scripts/stores/OutboundCommunicationStore.js b/app/scripts/stores/OutboundCommunicationStore.js
new file mode 100644
index 0000000000..6aa321d62c
--- /dev/null
+++ b/app/scripts/stores/OutboundCommunicationStore.js
@@ -0,0 +1,90 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+import { STATUS } from './common/Constants';
+var debug = require('debug')('AccountNotifStore:');
+
+export default createStore({
+ storeName: 'OutboundCommunicationStore',
+ handlers: {
+ RECEIVE_EMAIL_SUBSCRIPTIONS: '_receiveEmailSubscriptions',
+ RESET_OUTBOUND_EMAILS_STORE: '_resetBlankSlate',
+ SAVE_OUTBOUND_ERROR: '_saveOutboundError',
+ SAVE_OUTBOUND_SUCCESS: '_saveOutboundSuccess',
+ UPDATE_OUTBOUND: '_updateOutbound',
+ UPDATE_BETA_GROUP: '_updateBetaGroup'
+ },
+ initialize: function() {
+ // initialize with data from db
+ /*eslint-disable camelcase */
+ this.weeklyDigest = {
+ subscribed_emails: [],
+ unsubscribed_emails: []
+ };
+ this.digestEmails = [];
+ this.betaGroup = {
+ subscribed_emails: [],
+ unsubscribed_emails: []
+ };
+ /*eslint-enable camelcase */
+ this.betaEmails = [];
+ this.STATUS = STATUS.DEFAULT;
+ this.blankOutboundSlate = {};
+ },
+ _receiveEmailSubscriptions: function(payload) {
+ this.weeklyDigest = payload.weeklyDigest;
+ this.digestEmails = payload.weeklyDigest.subscribed_emails.concat(payload.weeklyDigest.unsubscribed_emails);
+ this.betaGroup = payload.betaGroup;
+ this.betaEmails = payload.betaGroup.subscribed_emails.concat(payload.betaGroup.unsubscribed_emails);
+ this.blankOutboundSlate = this.getState();
+ this.emitChange();
+ },
+ _resetBlankSlate: function() {
+ var slate = this.blankOutboundSlate;
+ debug('RESET OUTBOUND BLANK SLATE');
+ this.weeklyDigest = slate.weeklyDigest;
+ this.digestEmails = slate.digestEmails;
+ this.betaGroup = slate.betaGroup;
+ this.betaEmails = slate.betaEmails;
+ this.STATUS = STATUS.DEFAULT;
+ this.emitChange();
+ },
+ _updateOutbound: function(newList) {
+ this.STATUS = STATUS.DEFAULT;
+ if (newList.list === 'weekly') {
+ this.weeklyDigest = newList.data;
+ } else if (newList.list === 'beta') {
+ this.betaGroup = newList.data;
+ }
+ this.emitChange();
+ },
+ _saveOutboundError: function() {
+ this.STATUS = 'ERROR';
+ this.emitChange();
+ },
+ _saveOutboundSuccess: function() {
+ this.STATUS = STATUS.SUCCESSFUL;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ weeklyDigest: this.weeklyDigest,
+ digestEmails: this.digestEmails,
+ betaGroup: this.betaGroup,
+ betaEmails: this.betaEmails,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return _.merge({}, this.getState(), {blankOutboundSlate: this.blankOutboundSlate});
+ },
+ rehydrate: function(state) {
+ this.weeklyDigest = state.weeklyDigest;
+ this.digestEmails = state.digestEmails;
+ this.betaGroup = state.betaGroup;
+ this.betaEmails = state.betaEmails;
+ this.STATUS = state.STATUS;
+ this.blankOutboundSlate = state.blankOutboundSlate;
+ }
+});
diff --git a/app/scripts/stores/PipelineHistoryStore.js b/app/scripts/stores/PipelineHistoryStore.js
new file mode 100644
index 0000000000..59b9494f2d
--- /dev/null
+++ b/app/scripts/stores/PipelineHistoryStore.js
@@ -0,0 +1,31 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+const debug = require('debug')('PipelineHistory');
+
+export default createStore({
+ storeName: 'PipelineHistoryStore',
+ handlers: {
+ RECEIVE_PIPELINE_HISTORY: '_receivePipelineHistory'
+ },
+ initialize() {
+ this.results = {};
+ },
+ _receivePipelineHistory(data) {
+ this.results[data.slug] = {};
+ this.results[data.slug].results = data.payload.results;
+ this.results[data.slug].count = data.payload.count;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ results: this.results
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.results = state.results;
+ }
+});
diff --git a/app/scripts/stores/PlansStore.js b/app/scripts/stores/PlansStore.js
new file mode 100644
index 0000000000..8ac12e6c89
--- /dev/null
+++ b/app/scripts/stores/PlansStore.js
@@ -0,0 +1,74 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('PlansStore');
+
+var PlansStore = createStore({
+ storeName: 'PlansStore',
+ handlers: {
+ RECEIVE_BILLING_PLANS: '_receivePlans',
+ RECEIVE_BILLING_INFO: '_receiveBilling',
+ RECEIVE_BILLING_SUBSCRIPTION: '_receiveBillingSubscription',
+ RESET_CURRENT_PLAN: '_resetCurrentPlan'
+ },
+ initialize: function() {
+ this.plansList = [];
+ this.currentPlan = {
+ plan: '',
+ package: '',
+ subscription_uuid: '',
+ state: '',
+ add_ons: []
+ };
+ },
+ _clearPlan: function() {
+ this.currentPlan = {
+ plan: '',
+ package: '',
+ subscription_uuid: '',
+ state: '',
+ add_ons: []
+ };
+ },
+ _receiveBilling: function(payload){
+ debug('RECEIVE BILLING: ', payload);
+ this._clearPlan();
+ if (payload.currentPlan) {
+ this.currentPlan = payload.currentPlan;
+ }
+ this.emitChange();
+ },
+ _receiveBillingSubscription: function(payload) {
+ this._clearPlan();
+ if (payload.currentPlan) {
+ this.currentPlan = payload.currentPlan;
+ }
+ this.emitChange();
+ },
+ _resetCurrentPlan: function(payload) {
+ this._clearPlan();
+ if (payload.currentPlan) {
+ this.currentPlan = payload.currentPlan;
+ }
+ this.emitChange();
+ },
+ _receivePlans: function(payload) {
+ debug(payload);
+ this.plansList = payload.plansList;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ plansList: this.plansList,
+ currentPlan: this.currentPlan
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.plansList = state.plansList;
+ this.currentPlan = state.currentPlan;
+ }
+});
+
+module.exports = PlansStore;
diff --git a/app/scripts/stores/PrivateRepoUsageStore.js b/app/scripts/stores/PrivateRepoUsageStore.js
new file mode 100644
index 0000000000..ab21e79cc1
--- /dev/null
+++ b/app/scripts/stores/PrivateRepoUsageStore.js
@@ -0,0 +1,63 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('PrivateReposUsageStore');
+
+var PrivateRepoUsageStore = createStore({
+ storeName: 'PrivateRepoUsageStore',
+ handlers: {
+ RECEIVE_PRIVATE_REPOSTATS: '_receivePrivateRepoStats',
+ PRIVATE_REPOSTATS_NO_PERMISSIONS: '_notAvailable'
+ },
+ initialize: function() {
+ this.privateRepoUsed = 0;
+ this.numFreePrivateRepos = 0;
+ this.defaultRepoVisibility = 'public';
+ this.privateRepoAvailable = 0;
+ this.privateRepoPercentUsed = 0;
+ this.privateRepoLimit = 0;
+ this.notAvailable = false;
+ },
+ _receivePrivateRepoStats: function(stats) {
+ this.notAvailable = false;
+ /*eslint-disable camelcase */
+ this.privateRepoUsed = stats.private_repo_used;
+ this.numFreePrivateRepos = stats.num_free_private_repos;
+ this.defaultRepoVisibility = stats.default_repo_visibility;
+ this.privateRepoAvailable = stats.private_repo_available;
+ this.privateRepoPercentUsed = stats.private_repo_percent_used;
+ this.privateRepoLimit = stats.private_repo_limit;
+ /*eslint-enable camelcase */
+ this.emitChange();
+ },
+ _notAvailable: function(err) {
+ //No permissions to see the private repo stats for this org
+ this.notAvailable = true;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ privateRepoUsed: this.privateRepoUsed,
+ numFreePrivateRepos: this.numFreePrivateRepos,
+ defaultRepoVisibility: this.defaultRepoVisibility,
+ privateRepoAvailable: this.privateRepoAvailable,
+ privateRepoPercentUsed: this.privateRepoPercentUsed,
+ privateRepoLimit: this.privateRepoLimit,
+ notAvailable: this.notAvailable
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.privateRepoUsed = state.privateRepoUsed;
+ this.numFreePrivateRepos = state.numFreePrivateRepos;
+ this.defaultRepoVisibility = state.defaultRepoVisibility;
+ this.privateRepoAvailable = state.privateRepoAvailable;
+ this.privateRepoPercentUsed = state.privateRepoPercentUsed;
+ this.privateRepoLimit = state.privateRepoLimit;
+ this.notAvailable = state.notAvailable;
+ }
+});
+
+module.exports = PrivateRepoUsageStore;
diff --git a/app/scripts/stores/RepoDetailsBuildLogs.js b/app/scripts/stores/RepoDetailsBuildLogs.js
new file mode 100644
index 0000000000..9d263b87b7
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsBuildLogs.js
@@ -0,0 +1,29 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('RepoDetailsBuildLogsStore');
+
+export default createStore({
+ storeName: 'RepoDetailsBuildLogsStore',
+ handlers: {
+ BUILD_LOGS_RECEIVE: '_receiveBuildLogs'
+ },
+ initialize() {
+ this.build_results = {};
+ },
+ _receiveBuildLogs(res) {
+ this.build_results = res.build_results;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ build_results: this.build_results
+ };
+ },
+ rehydrate(state) {
+ this.build_results = state.build_results;
+ },
+ dehydrate() {
+ return this.getState();
+ }
+});
diff --git a/app/scripts/stores/RepoDetailsBuildsStore.js b/app/scripts/stores/RepoDetailsBuildsStore.js
new file mode 100644
index 0000000000..793feeddad
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsBuildsStore.js
@@ -0,0 +1,79 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import find from 'lodash/collection/find';
+import map from 'lodash/collection/map';
+import assign from 'lodash/object/assign';
+
+const debug = require('debug')('RepoDetailsBuildsStore');
+
+const RepoDetailsBuildsStore = createStore({
+ storeName: 'RepoDetailsBuildsStore',
+ handlers: {
+ RECEIVE_BUILD_HISTORY_FOR_REPOSITORY: '_receiveBuilds',
+ CANCEL_BUILD_START: '_cancelBuildStart',
+ CANCEL_BUILD_SUCCESS: '_cancelBuildSuccess',
+ CANCEL_BUILD_ERROR: '_cancelBuildError'
+ }
+ ,
+ initialize() {
+ this.results = [];
+ this.canceling = {};
+ this.count = 0;
+ },
+
+ _cancelBuildStart(id) {
+ this.canceling = {
+ ...this.canceling,
+ [id]: 'queued'
+ };
+ this.emitChange();
+ },
+
+ _cancelBuildSuccess(id) {
+ this.canceling = {
+ ...this.canceling,
+ [id]: 'success'
+ };
+ this.emitChange();
+ },
+
+ _cancelBuildError({ id, detail }) {
+ this.canceling = {
+ ...this.canceling,
+ [id]: 'failed'
+ };
+ this.emitChange();
+ },
+
+ _receiveBuilds(res) {
+ debug('receiving builds', res);
+ this.results = res.results;
+ this.count = res.count;
+ this.emitChange();
+ },
+
+ getState: function() {
+ let results = this.results;
+ if (this.canceling !== undefined) {
+ results = map(this.results, (v) => assign(v, { canceling: this.canceling[v.id] }));
+ }
+ return {
+ count: this.count,
+ canceling: this.canceling,
+ results
+ };
+ },
+
+ rehydrate(state) {
+ this.results = state.results;
+ this.count = state.count;
+ this.canceling = state.canceling || {};
+ },
+
+ dehydrate() {
+ return this.getState();
+ }
+});
+
+export default RepoDetailsBuildsStore;
diff --git a/app/scripts/stores/RepoDetailsDockerfileStore.js b/app/scripts/stores/RepoDetailsDockerfileStore.js
new file mode 100644
index 0000000000..ed1a1aee22
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsDockerfileStore.js
@@ -0,0 +1,32 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('RepoDetailsDockerfileStore');
+
+var RepoDetailsDockerfileStore = createStore({
+ storeName: 'RepoDetailsDockerfileStore',
+ handlers: {
+ RECEIVE_DOCKERFILE_FOR_REPOSITORY: '_receiveDockerfile'
+ },
+ initialize: function() {
+ this.dockerfile = '';
+ },
+ _receiveDockerfile: function(res) {
+ debug('dockerfile', res);
+ this.dockerfile = res.contents;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ dockerfile: this.dockerfile
+ };
+ },
+ rehydrate: function(state) {
+ this.dockerfile = state.dockerfile;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = RepoDetailsDockerfileStore;
diff --git a/app/scripts/stores/RepoDetailsLongDescriptionFormStore.js b/app/scripts/stores/RepoDetailsLongDescriptionFormStore.js
new file mode 100644
index 0000000000..d9ce6e5b60
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsLongDescriptionFormStore.js
@@ -0,0 +1,116 @@
+'use strict';
+
+import _ from 'lodash';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('RepoDetailsLongDescriptionFormStore');
+
+export default createStore({
+ storeName: 'RepoDetailsLongDescriptionFormStore',
+ handlers: {
+ RECEIVE_REPOSITORY: '_receiveRepository',
+ LONG_DESCRIPTION_ATTEMPT_START: '_longDescriptionAttemptStart',
+ LONG_DESCRIPTION_SUCCESS: '_longDescriptionSuccess',
+ DETAILS_UNAUTHORIZED: '_detailsUnauthorized',
+ DETAILS_UNAUTHORIZED_DETAIL: '_detailsUnauthorizedDetail',
+ LONG_BAD_REQUEST: '_badRequest',
+ DETAILS_ERROR: '_detailsError',
+ LONG_DESCRIPTION_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ DETAILS_RESET_FORMS: '_detailsResetForms',
+ TOGGLE_LONG_DESCRIPTION_EDIT: '_toggleEditMode'
+ },
+ initialize: function() {
+ this.isEditing = false;
+ this.successfulSave = false;
+ this.fields = {
+ longDescription: {}
+ };
+
+ this._defaultValues = {
+ longDescription: ''
+ };
+
+ this.values = {
+ longDescription: ''
+ };
+ },
+ _longDescriptionAttemptStart() {
+ debug('starting long description update');
+ },
+ _longDescriptionSuccess() {
+ this.fields.longDescription.success = 'Successfully updated full description.';
+ //switch back to viewing mode with green outline
+ this.successfulSave = true;
+ this.isEditing = false;
+ this._defaultValues.longDescription = this.values.longDescription;
+ //clear the green outline and text on successful save
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ setTimeout(this._clearSuccessfulSave.bind(this), 5000);
+ this.emitChange();
+ },
+ _receiveRepository(repo) {
+ this.isEditing = false;
+ this.values.longDescription = repo.full_description || '';
+ this._defaultValues.longDescription = repo.full_description || '';
+ this.emitChange();
+ },
+ _detailsResetForms() {
+ // reset form value to repo longdescription
+ this.values.longDescription = this._defaultValues.longDescription;
+ // reset errors
+ this.fields.longDescription = {};
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ this.fields.longDescription.hasError = !!obj.full_description;
+ this.fields.longDescription.error = obj.full_description[0];
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _detailsError() {
+ this.fields.longDescription.hasError = true;
+ this.fields.longDescription.error = 'Sorry, your long description could not be saved.';
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearFeedbackStates() {
+ this.fields.longDescription.error = '';
+ this.fields.longDescription.hasError = false;
+ this.fields.longDescription.success = '';
+ this.emitChange();
+ },
+ _clearSuccessfulSave() {
+ this.successfulSave = false;
+ this.emitChange();
+ },
+ _updateFieldWithValue: function({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _toggleEditMode( { isEditing }) {
+ this.isEditing = isEditing;
+ //if you cancel, clear the old input
+ this.values.longDescription = this._defaultValues.longDescription;
+ //in case you recently saved and you now cancel
+ this._clearSuccessfulSave();
+ this.emitChange();
+ },
+ getState() {
+ return {
+ _defaultValues: this._defaultValues,
+ fields: this.fields,
+ values: this.values,
+ isEditing: this.isEditing,
+ successfulSave: this.successfulSave
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this._defaultValues = state._defaultValues;
+ this.fields = state.fields;
+ this.values = state.values;
+ this.isEditing = state.isEditing;
+ this.successfulSave = state.successfulSave;
+ }
+});
diff --git a/app/scripts/stores/RepoDetailsShortDescriptionFormStore.js b/app/scripts/stores/RepoDetailsShortDescriptionFormStore.js
new file mode 100644
index 0000000000..1f573f12cb
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsShortDescriptionFormStore.js
@@ -0,0 +1,116 @@
+'use strict';
+
+import _ from 'lodash';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('RepoDetailsShortDescriptionFormStore');
+
+export default createStore({
+ storeName: 'RepoDetailsShortDescriptionFormStore',
+ handlers: {
+ RECEIVE_REPOSITORY: '_receiveRepository',
+ SHORT_DESCRIPTION_ATTEMPT_START: '_shortDescriptionAttemptStart',
+ SHORT_DESCRIPTION_SUCCESS: '_shortDescriptionSuccess',
+ DETAILS_UNAUTHORIZED: '_detailsUnauthorized',
+ DETAILS_UNAUTHORIZED_DETAIL: '_detailsUnauthorizedDetail',
+ SHORT_BAD_REQUEST: '_badRequest',
+ DETAILS_ERROR: '_detailsError',
+ SHORT_DESCRIPTION_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ DETAILS_RESET_FORMS: '_detailsResetForms',
+ TOGGLE_SHORT_DESCRIPTION_EDIT: '_toggleEditMode'
+ },
+ initialize: function() {
+ this.isEditing = false;
+ this.successfulSave = false;
+ this.fields = {
+ shortDescription: {}
+ };
+
+ this._defaultValues = {
+ shortDescription: ''
+ };
+
+ this.values = {
+ shortDescription: ''
+ };
+ },
+ _shortDescriptionAttemptStart() {
+ debug('starting short description update');
+ },
+ _shortDescriptionSuccess() {
+ this.fields.shortDescription.success = 'Successfully updated short description.';
+ //switch back to viewing mode with green outline
+ this.successfulSave = true;
+ this.isEditing = false;
+ this._defaultValues.shortDescription = this.values.shortDescription;
+ //clear the green outline and text on successful save
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ setTimeout(this._clearSuccessfulSave.bind(this), 5000);
+ this.emitChange();
+ },
+ _receiveRepository(repo) {
+ this.isEditing = false;
+ this.values.shortDescription = repo.description;
+ this._defaultValues.shortDescription = repo.description;
+ this.emitChange();
+ },
+ _detailsResetForms() {
+ // reset form value to repo shortdescription
+ this.values.shortDescription = this._defaultValues.shortDescription;
+ // reset errors
+ this.fields.shortDescription = {};
+ this.emitChange();
+ },
+ _detailsError() {
+ this.fields.longDescription.hasError = true;
+ this.fields.longDescription.error = 'Sorry, your long description could not be saved.';
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ this.fields.shortDescription.hasError = true;
+ this.fields.shortDescription.error = obj.description[0];
+ setTimeout(this._clearFeedbackStates.bind(this), 5000);
+ this.emitChange();
+ },
+ _clearFeedbackStates() {
+ this.fields.shortDescription.error = '';
+ this.fields.shortDescription.hasError = false;
+ this.fields.shortDescription.success = '';
+ this.emitChange();
+ },
+ _clearSuccessfulSave() {
+ this.successfulSave = false;
+ this.emitChange();
+ },
+ _updateFieldWithValue: function({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ },
+ _toggleEditMode( { isEditing }) {
+ this.isEditing = isEditing;
+ //if you cancel, clear the old input
+ this.values.shortDescription = this._defaultValues.shortDescription;
+ //in case you recently saved and you now cancel
+ this._clearSuccessfulSave();
+ this.emitChange();
+ },
+ getState() {
+ return {
+ _defaultValues: this._defaultValues,
+ fields: this.fields,
+ values: this.values,
+ isEditing: this.isEditing,
+ successfulSave: this.successfulSave
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this._defaultValues = state._defaultValues;
+ this.fields = state.fields;
+ this.values = state.values;
+ this.isEditing = state.isEditing;
+ this.successfulSave = state.successfulSave;
+ }
+});
diff --git a/app/scripts/stores/RepoDetailsVisibilityFormStore.js b/app/scripts/stores/RepoDetailsVisibilityFormStore.js
new file mode 100644
index 0000000000..1ef9872139
--- /dev/null
+++ b/app/scripts/stores/RepoDetailsVisibilityFormStore.js
@@ -0,0 +1,113 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { STATUS } from './repovisibilitystore/Constants.js';
+const debug = require('debug')('RepoDetailsVisibilityFormStore');
+
+var RepoDetailsVisibilityFormStore = createStore({
+ storeName: 'RepoDetailsVisibilityFormStore',
+ handlers: {
+ VISIBILITY_BAD_REQUEST: '_badRequest',
+ VISIBILITY_ERROR: '_visibilityError',
+ TOGGLE_VISIBILITY_ATTEMPT_START: '_toggleVisibilityAttemptStart',
+ TOGGLE_VISIBILITY_SUCCESS: '_toggleSuccess',
+ RECEIVE_PRIVATE_REPOSTATS: '_receivePrivateRepoStats',
+ RECEIVE_REPOSITORY: '_receiveRepository',
+ REPO_DETAILS_VISIBILITY_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ TOGGLE_VISIBILITY_REPO_NAME_CONFIRM_BOX: '_toggleConfirmBox'
+ },
+ initialize: function() {
+ this.badRequest = '';
+ this.error = '';
+ this.success = '';
+ this.isPrivate = false;
+ this.privateRepoLimit = null;
+ this.numPrivateReposAvailable = null;
+ this.STATUS = STATUS.DEFAULT;
+ this.values = {
+ confirmRepoName: ''
+ };
+ },
+ _badRequest: function(res) {
+ this.initialize();
+ this.badRequest = res.detail;
+ this.STATUS = STATUS.FORM_ERROR;
+ this.emitChange();
+ },
+ _clearErrors: function () {
+ this.error = '';
+ this.badRequest = '';
+ },
+ _toggleVisibilityAttemptStart: function() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _visibilityError: function(maybeError) {
+ this.STATUS = STATUS.FORM_ERROR;
+ if (maybeError) {
+ this.error = maybeError.detail;
+ } else {
+ this.error = 'No private repositories available';
+ }
+ this.emitChange();
+ },
+ _toggleSuccess: function(isPrivate) {
+ this.initialize();
+ this.isPrivate = isPrivate;
+ this.emitChange();
+ },
+ _toggleConfirmBox: function() {
+ if (this.STATUS === STATUS.DEFAULT) {
+ this.STATUS = STATUS.SHOWING_CONFIRM_BOX;
+ } else {
+ this.STATUS = STATUS.DEFAULT;
+ }
+ this._clearErrors();
+ this.values.confirmRepoName = '';
+ this.emitChange();
+ },
+ _receivePrivateRepoStats: function(stats) {
+ /*eslint-disable camelcase */
+ this.numPrivateReposAvailable = stats.private_repo_available;
+ this.privateRepoLimit = stats.private_repo_limit;
+ /*eslint-enable camelcase */
+ this.emitChange();
+ },
+ _receiveRepository: function(res) {
+ this.initialize();
+ this.isPrivate = res.is_private;
+ this.emitChange();
+ },
+ _updateFieldWithValue: function({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ this._clearErrors();
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ badRequest: this.badRequest,
+ error: this.error,
+ success: this.success,
+ isPrivate: this.isPrivate,
+ privateRepoLimit: this.privateRepoLimit,
+ numPrivateReposAvailable: this.numPrivateReposAvailable,
+ values: this.values,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.badRequest = state.badRequest;
+ this.error = state.error;
+ this.success = state.success;
+ this.isPrivate = state.isPrivate;
+ this.numPrivateReposAvailable = state.numPrivateReposAvailable;
+ this.privateRepoLimit = state.privateRepoLimit;
+ this.values = state.values;
+ this.STATUS = state.STATUS;
+ }
+});
+
+module.exports = RepoDetailsVisibilityFormStore;
diff --git a/app/scripts/stores/RepoSettingsCollaborators.jsx b/app/scripts/stores/RepoSettingsCollaborators.jsx
new file mode 100644
index 0000000000..20705e93ea
--- /dev/null
+++ b/app/scripts/stores/RepoSettingsCollaborators.jsx
@@ -0,0 +1,104 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import { STATUS } from './collaborators/Constants.js';
+var debug = require('debug')('RepoSettingsCollaborators');
+
+export default createStore({
+ storeName: 'RepoSettingsCollaborators',
+ handlers: {
+ ADD_COLLAB_START: '_addCollabStart',
+ ADD_COLLAB_ERROR: '_addCollabError',
+ ADD_COLLAB_SUCCESS: '_addCollabSuccess',
+ COLLAB_RECEIVE_COLLABORATORS: '_receiveCollaborators',
+ COLLAB_RECEIVE_TEAMS: '_receiveTeams',
+ COLLAB_RECEIVE_ALL_TEAMS: '_receiveAllTeams',
+ DEL_COLLABORATORS_SET_LOADING: 'setLoadingFor',
+ DEL_COLLABORATORS_SET_ERROR: 'setErrorFor',
+ DEL_COLLABORATORS_SET_SUCCESS: 'setSuccessFor',
+ LOGOUT: 'initialize',
+ ON_ADD_COLLAB_CHANGE: 'onAddCollabChange'
+ },
+ initialize() {
+ // these are full request objects. Only one will succeed and have a `count` key
+ this.collaborators = {};
+ this.teams = {};
+ this.allTeams = {results: []};
+ this.newCollaborator = '';
+ this.error = '';
+ this.requests = {};
+ this.STATUS = STATUS.DEFAULT;
+ },
+ getState() {
+ return {
+ collaborators: this.collaborators,
+ teams: this.teams,
+ allTeams: this.allTeams,
+ newCollaborator: this.newCollaborator,
+ error: this.error,
+ requests: this.requests,
+ STATUS: this.STATUS
+ };
+ },
+
+ setLoadingFor(username) {
+ this.requests[username] = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ setErrorFor(username) {
+ this.requests[username] = STATUS.ERROR;
+ this.emitChange();
+ },
+ setSuccessFor(username) {
+ this.requests[username] = STATUS.DEFAULT;
+ this.newCollaborator = '';
+ this.emitChange();
+ },
+ onAddCollabChange(collaborator) {
+ this.newCollaborator = collaborator;
+ this.error = '';
+ this.emitChange();
+ },
+ _addCollabSuccess() {
+ this.STATUS = STATUS.SUCCESS;
+ this.newCollaborator = '';
+ this.error = '';
+ this.emitChange();
+ },
+ _addCollabError(message) {
+ this.STATUS = STATUS.ERROR;
+ this.error = message;
+ this.emitChange();
+ },
+ _addCollabStart() {
+ this.STATUS = STATUS.ATTEMPTING;
+ this.emitChange();
+ },
+ _receiveCollaborators(collaborators) {
+ debug(collaborators);
+ this.newCollaborator = '';
+ this.collaborators = collaborators;
+ this.emitChange();
+ },
+ _receiveTeams(teams) {
+ debug(teams);
+ this.teams = teams;
+ this.emitChange();
+ },
+ _receiveAllTeams(teams) {
+ this.allTeams = teams;
+ this.emitChange();
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.collaborators = state.collaborators;
+ this.teams = state.teams;
+ this.allTeams = state.allTeams;
+ this.newCollaborator = state.newCollaborator;
+ this.error = state.error;
+ this.requests = state.requests;
+ this.STATUS = state.STATUS;
+ }
+});
diff --git a/app/scripts/stores/RepositoriesPageStore.js b/app/scripts/stores/RepositoriesPageStore.js
new file mode 100644
index 0000000000..9e82aa2480
--- /dev/null
+++ b/app/scripts/stores/RepositoriesPageStore.js
@@ -0,0 +1,42 @@
+'use strict';
+
+import createStore from'fluxible/addons/createStore';
+
+var ReposStore = createStore({
+ storeName: 'RepositoriesPageStore',
+ handlers: {
+ RECEIVE_REPOS: '_receiveRepos'
+ },
+ initialize: function() {
+ this.repos = [];
+ this.previous = null;
+ this.next = null;
+ this.count = null;
+ },
+ _receiveRepos: function(res) {
+ this.repos = res.results;
+ this.previous = res.previous;
+ this.next = res.next;
+ this.count = res.count;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ repos: this.repos,
+ previous: this.previous,
+ next: this.next,
+ count: this.count
+ };
+ },
+ rehydrate: function(state) {
+ this.repos = state.repos;
+ this.previous = state.previous;
+ this.next = state.next;
+ this.count = state.count;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = ReposStore;
diff --git a/app/scripts/stores/RepositoryCommentsStore.js b/app/scripts/stores/RepositoryCommentsStore.js
new file mode 100644
index 0000000000..f31d5af4e4
--- /dev/null
+++ b/app/scripts/stores/RepositoryCommentsStore.js
@@ -0,0 +1,42 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('RepositoryCommentsStore');
+
+var RepoCommentsStore = createStore({
+ storeName: 'RepositoryCommentsStore',
+ handlers: {
+ RECEIVE_REPO_COMMENTS: '_receiveRepoComments'
+ },
+ initialize: function() {
+ this.results = [];
+ this.prev = null;
+ this.next = null;
+ this.count = 0;
+ },
+ _receiveRepoComments: function(res) {
+ this.results = res.results;
+ this.prev = res.previous;
+ this.next = res.next;
+ this.count = res.count;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ results: this.results,
+ prev: this.prev,
+ next: this.next,
+ count: this.count
+ };
+ },
+ rehydrate: function(state) {
+ this.results = state.results;
+ this.prev = state.prev;
+ this.next = state.next;
+ this.count = state.count;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = RepoCommentsStore;
diff --git a/app/scripts/stores/RepositoryPageStore.js b/app/scripts/stores/RepositoryPageStore.js
new file mode 100644
index 0000000000..64a6866b84
--- /dev/null
+++ b/app/scripts/stores/RepositoryPageStore.js
@@ -0,0 +1,118 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+const STATUS = require('./repostore/Constants').STATUS;
+var debug = require('debug')('RepositoryPageStore');
+
+var RepoStore = createStore({
+ storeName: 'RepositoryPageStore',
+ handlers: {
+ RECEIVE_REPOSITORY: '_receiveRepository',
+ CREATE_REPO_ERROR: '_createRepoError',
+ TOGGLE_STARRED_STATE: '_toggleStarred',
+ TOGGLE_VISIBILITY_SUCCESS: '_toggleVisibility',
+ REPO_NOT_FOUND: '_repoNotFound'
+ },
+ initialize: function() {
+ this.canEdit = false;
+ this.description = '';
+ this.fullDescription = '';
+ this.hasStarred = false;
+ this.isPrivate = true;
+ this.isAutomated = false;
+ this.name = '';
+ this.namespace = '';
+ this.status = 0;
+ this.lastUpdated = '';
+ this.globalFormError = '';
+ this.STATUS = STATUS.DEFAULT;
+ },
+ _createRepoError: function(err) {
+ if (err) {
+ var errResponse = err.response.body;
+ this.globalFormError = '';
+ if (!_.isEmpty(errResponse)) {
+ if (err.response.badRequest) {
+ this.STATUS = STATUS.BAD_REQUEST;
+ if (_.has(errResponse, '__all__')) {
+ this.globalFormError = errResponse.__all__.toString();
+ } else if (_.has(errResponse, 'detail')) {
+ this.globalFormError = errResponse.detail.toString();
+ } else {
+ _.forIn(errResponse, function(v, k) {
+ this.globalFormError += k + ': ' + v.join(',') + '\n';
+ }.bind(this));
+ }
+ }
+ } else {
+ this.STATUS = STATUS.GENERAL_SERVER_ERROR;
+ this.globalFormError = 'An error occurred while creating your repository. Please try again later.';
+ }
+ }
+ this.emitChange();
+ },
+ _receiveRepository: function(res) {
+ debug('receive repo', res);
+ this.STATUS = STATUS.DEFAULT;
+ this.canEdit = res.can_edit;
+ this.description = res.description;
+ // full_description can come in as null; Default to string
+ this.fullDescription = res.full_description || '';
+ this.hasStarred = res.has_starred;
+ this.isPrivate = res.is_private;
+ this.isAutomated = res.is_automated;
+ this.lastUpdated = res.last_updated;
+ this.name = res.name;
+ this.namespace = res.namespace;
+ this.status = res.status;
+
+ this.emitChange();
+ },
+ _toggleStarred: function(status) {
+ this.hasStarred = status;
+ this.emitChange();
+ },
+ _toggleVisibility: function(vis) {
+ this.isPrivate = vis;
+ this.emitChange();
+ },
+ _repoNotFound: function(err) {
+ this.STATUS = STATUS.REPO_NOT_FOUND;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ canEdit: this.canEdit,
+ description: this.description,
+ fullDescription: this.fullDescription,
+ hasStarred: this.hasStarred,
+ isPrivate: this.isPrivate,
+ isAutomated: this.isAutomated,
+ lastUpdated: this.lastUpdated,
+ name: this.name,
+ namespace: this.namespace,
+ status: this.status,
+ globalFormError: this.globalFormError,
+ STATUS: this.STATUS
+ };
+ },
+ rehydrate: function(state) {
+ this.canEdit = state.canEdit;
+ this.description = state.description;
+ this.fullDescription = state.fullDescription;
+ this.hasStarred = state.hasStarred;
+ this.isPrivate = state.isPrivate;
+ this.isAutomated = state.isAutomated;
+ this.name = state.name;
+ this.lastUpdated = state.lastUpdated;
+ this.namespace = state.namespace;
+ this.status = state.status;
+ this.globalFormError = state.globalFormError;
+ this.STATUS = state.STATUS;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = RepoStore;
diff --git a/app/scripts/stores/SearchStore.js b/app/scripts/stores/SearchStore.js
new file mode 100644
index 0000000000..0884946f82
--- /dev/null
+++ b/app/scripts/stores/SearchStore.js
@@ -0,0 +1,104 @@
+'use strict';
+const createStore = require('fluxible/addons/createStore');
+const _ = require('lodash');
+const debug = require('debug')('STORE:SearchStore');
+
+//Store to keep track of searches
+//TODO: Autocomplete support?
+//Query API on general search and return results in a Search Results Component
+var SearchStore = createStore({
+ storeName: 'SearchStore',
+ handlers: {
+ SUBMIT_SEARCH_QUERY: '_submitSearchQuery',
+ UPDATE_SEARCH_FILTER: '_updateSearchFilter',
+ UPDATE_SEARCH_SORT: '_updateSearchSort',
+ UPDATE_SEARCH_PAGE: '_updateSearchPage',
+ UPDATE_SEARCH_OTHERFILTERS: '_updateSearchFilters',
+ PROCESS_SEARCH_RESULTS: '_processSearchResults',
+ SEARCH_ERROR: '_handleSearchError'
+ },
+ initialize: function() {
+ this.results = '';
+ this.queryResult = {};
+ this.page = 1;
+ this.count = 0;
+ this.next = false;
+ this.prev = false;
+ },
+ getQueryParams: function() {
+ //transition to will always have `q` appended as query param at the very least
+ //Other query params like: `s` -> sort by | `t=User` -> user | `t=Organization` -> Org | `f=official`
+ // `f=automated_builds` | `s=date_created`, `s=last_updated`, `s=alphabetical`, `s=stars`, `s=downloads`
+ // `s=pushes`
+ var queryParams = {
+ q: this.query || '',
+ page: this.page || 1,
+ isOfficial: this.isOfficial || 0,
+ isAutomated: this.isAutomated || 0,
+ pullCount: this.pullCount || 0,
+ starCount: this.starCount || 0
+ };
+ return queryParams;
+ },
+ _submitSearchQuery: function(payload: string) {
+ this.query = payload;
+ this.results = null;
+ this.emitChange();
+ },
+ _processSearchResults: function(searchResult) {
+ this.queryResult = searchResult;
+ this.count = searchResult.count;
+ this.results = searchResult.results;
+ this.next = searchResult.next;
+ this.prev = searchResult.previous;
+ this.emitChange();
+ },
+ _handleSearchError: function(searchError) {
+ //TODO: Some form of common error handling across all components
+ debug(searchError);
+ },
+ _updateSearchPage: function(page) {
+ this.page = page;
+ this.emitChange();
+ },
+ _updateSearchFilters: function(params) {
+ this.isAutomated = params.isAutomated;
+ this.isOfficial = params.isOfficial;
+ this.pullCount = params.pullCount;
+ this.starCount = params.starCount;
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ query: this.query,
+ page: this.page,
+ queryResult: this.queryResult,
+ results: this.results,
+ isOfficial: this.isOfficial,
+ isAutomated: this.isAutomated,
+ pullCount: this.pullCount,
+ starCount: this.starCount,
+ count: this.count,
+ next: this.next,
+ prev: this.prev
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.query = state.query;
+ this.page = state.page;
+ this.queryResult = state.queryResult;
+ this.results = state.results;
+ this.isOfficial = state.isOfficial;
+ this.isAutomated = state.isAutomated;
+ this.pullCount = state.pullCount;
+ this.starCount = state.starCount;
+ this.count = state.count;
+ this.next = state.next;
+ this.prev = state.prev;
+ }
+});
+
+module.exports = SearchStore;
diff --git a/app/scripts/stores/SignupStore.js b/app/scripts/stores/SignupStore.js
new file mode 100644
index 0000000000..bedf00db7f
--- /dev/null
+++ b/app/scripts/stores/SignupStore.js
@@ -0,0 +1,117 @@
+'use strict';
+
+var createStore = require('fluxible/addons/createStore');
+import { STATUS } from './signupstore/Constants';
+var debug = require('debug')('SignupStore');
+var _ = require('lodash');
+
+var noErrorObj = {
+ hasError: false,
+ error: ''
+};
+
+export default createStore({
+ storeName: 'SignupStore',
+ handlers: {
+ SIGNUP_CLEAR_FORM: '_signupClearForm',
+ SIGNUP_CLEAR_PASSWORD: '_signupClearPassword',
+ SIGNUP_UPDATE_FIELD_WITH_VALUE: '_updateFieldWithValue',
+ SIGNUP_ATTEMPT_START: '_signupAttemptStart',
+ SIGNUP_BAD_REQUEST: '_badRequest',
+ SIGNUP_SUCCESS: '_signupSuccess'
+ },
+ initialize() {
+ this.STATUS = STATUS.DEFAULT;
+
+ this.fields = {
+ username: {},
+ email: {},
+ password: {}
+ };
+
+ this.values = {
+ username: '',
+ email: '',
+ password: ''
+ };
+ },
+ _signupClearForm() {
+ this.initialize();
+ this.emitChange();
+ },
+ _signupClearPassword() {
+ this.values.password = '';
+ this.emitChange();
+ },
+ _updateFieldWithValue({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ if (fieldValue) {
+ this.fields[fieldKey] = this._validate({fieldKey, fieldValue});
+ }
+ this.emitChange();
+ },
+ _signupAttemptStart() {
+ debug('attempting Signup');
+ this.STATUS = STATUS.ATTEMPTING_SIGNUP;
+ this.emitChange();
+ },
+ _signupSuccess() {
+ this.STATUS = STATUS.SUCCESSFUL_SIGNUP;
+ this.emitChange();
+ },
+ _badRequest(obj) {
+ let shouldEmitChange = false;
+
+ // cycle through the possible form fields
+ _.forEach(_.keys(this.fields),
+ (key) => {
+ if(_.has(obj, key)) {
+ shouldEmitChange = true;
+ var newField = {};
+ newField.hasError = !!obj[key];
+ newField.error = obj[key][0];
+ this.fields[key] = newField;
+ }
+ });
+ if(shouldEmitChange) {
+ this.emitChange();
+ }
+ },
+ validations: {
+ username(value) {
+ if (value.length < 4){
+ return {
+ hasError: true,
+ error: 'Username must be at least four characters long'
+ };
+ } else if (!/^[A-Za-z0-9]+$/.test(value)) {
+ return {
+ hasError: true,
+ error: 'Username must contain only letters and digits'
+ };
+ } else {
+ return noErrorObj;
+ }
+ }
+ },
+ _validate({fieldKey, fieldValue}) {
+ if(_.isFunction(this.validations[fieldKey])) {
+ return this.validations[fieldKey](fieldValue);
+ } else {
+ return noErrorObj;
+ }
+ },
+ getState() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate() {
+ return {};
+ },
+ rehydrate(state) {
+ this.state = state;
+ }
+});
diff --git a/app/scripts/stores/TriggerBuildStore.js b/app/scripts/stores/TriggerBuildStore.js
new file mode 100644
index 0000000000..dab896a28a
--- /dev/null
+++ b/app/scripts/stores/TriggerBuildStore.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const createStore = require('fluxible/addons/createStore');
+const debug = require('debug')('TriggerBuildStore');
+
+var TriggerBuildStore = createStore({
+ storeName: 'TriggerBuildStore',
+ handlers: {
+ AB_TRIGGER_SUCCESS: '_abTriggerSuccess',
+ AB_TRIGGER_ERROR: '_abTriggerError'
+ },
+ initialize: function() {
+ this.abtrigger = {
+ hasError: false,
+ success: false
+ };
+ },
+ _abTriggerClear: function() {
+ this.abtrigger = {
+ hasError: false,
+ success: false
+ };
+ this.emitChange();
+ },
+ _abTriggerError: function() {
+ this.abtrigger.success = false;
+ this.abtrigger.hasError = true;
+ setTimeout(this._abTriggerClear.bind(this), 3000);
+ this.emitChange();
+ },
+ _abTriggerSuccess: function() {
+ this.abtrigger.success = true;
+ this.abtrigger.hasError = false;
+ setTimeout(this._abTriggerClear.bind(this), 3000);
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ abtrigger: this.abtrigger
+ };
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ this.abtrigger = state.abtrigger;
+ }
+});
+
+module.exports = TriggerBuildStore;
diff --git a/app/scripts/stores/UnlinkAccountsStore.js b/app/scripts/stores/UnlinkAccountsStore.js
new file mode 100644
index 0000000000..22f7b2ad35
--- /dev/null
+++ b/app/scripts/stores/UnlinkAccountsStore.js
@@ -0,0 +1,36 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('UnlinkAccountsStore');
+
+var BitbucketLinkStore = createStore({
+ storeName: 'UnlinkAccountsStore',
+ handlers: {
+ GITHUB_UNLINK_ERROR: _unlinkGithubError,
+ BITBUCKET_UNLINK_ERROR: _unlinkBitbucketError
+ },
+ initialize: function() {
+ this.error = '';
+ },
+ _unlinkGithubError: function() {
+ this.error = 'Error unlinking Github Account. Please try again later';
+ this.emitChange();
+ },
+ _unlinkBitbucketError: function() {
+ this.error = 'Error unlinking Bitbucket Account. Please try again later';
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ error: this.error
+ };
+ },
+ rehydrate: function(state) {
+ this.error = state.error;
+ },
+ dehydrate: function() {
+ return this.getState();
+ }
+});
+
+module.exports = UnlinkAccountsStore;
diff --git a/app/scripts/stores/UserProfileReposStore.js b/app/scripts/stores/UserProfileReposStore.js
new file mode 100644
index 0000000000..397a727c14
--- /dev/null
+++ b/app/scripts/stores/UserProfileReposStore.js
@@ -0,0 +1,46 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import filter from 'lodash/collection/filter';
+import { PENDING_DELETE } from 'common/enums/RepoStatus';
+var debug = require('debug')('stores: UserProfileStore');
+
+export default createStore({
+ storeName: 'UserProfileReposStore',
+ handlers: {
+ RECEIVE_PROFILE_REPOS: 'receiveRepos'
+ },
+ initialize() {
+ this.repos = [];
+ this.next = null;
+ this.prev = null;
+ },
+ removePendingDeleteRepos(repos) {
+ //Remove repos that are in pending delete state from user profile repos
+ return filter(repos, (repo) => {
+ const { status } = repo;
+ return status !== PENDING_DELETE;
+ });
+ },
+ receiveRepos(res) {
+ this.repos = this.removePendingDeleteRepos(res.results);
+ this.next = res.next;
+ this.prev = res.previous;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ repos: this.repos,
+ next: this.next,
+ prev: this.prev
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.repos = state.repos;
+ this.next = state.next;
+ this.prev = state.prev;
+ }
+});
diff --git a/app/scripts/stores/UserProfileStarsStore.js b/app/scripts/stores/UserProfileStarsStore.js
new file mode 100644
index 0000000000..1a70b56588
--- /dev/null
+++ b/app/scripts/stores/UserProfileStarsStore.js
@@ -0,0 +1,37 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('stores: UserProfileStarsStore');
+
+export default createStore({
+ storeName: 'UserProfileStarsStore',
+ handlers: {
+ RECEIVE_PROFILE_STARRED_REPOS: '_receiveStarredRepos'
+ },
+ initialize() {
+ this.repos = [];
+ this.next = null;
+ this.prev = null;
+ },
+ _receiveStarredRepos(res) {
+ this.starred = res.results;
+ this.next = res.next;
+ this.prev = res.previous;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ starred: this.starred,
+ next: this.next,
+ prev: this.prev
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.starred = state.starred;
+ this.next = state.next;
+ this.prev = state.prev;
+ }
+});
diff --git a/app/scripts/stores/UserProfileStore.js b/app/scripts/stores/UserProfileStore.js
new file mode 100644
index 0000000000..a4c44484e7
--- /dev/null
+++ b/app/scripts/stores/UserProfileStore.js
@@ -0,0 +1,38 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+var debug = require('debug')('UserProfileStore');
+
+export default createStore({
+ storeName: 'UserProfileStore',
+ handlers: {
+ RECEIVE_PROFILE_USER: '_receiveUser',
+ USER_PROFILE_404: '_fourOHfour'
+ },
+ initialize() {
+ this.STATUS = 'DEFAULT';
+ this.user = {};
+ },
+ _fourOHfour() {
+ this.STATUS = '404';
+ this.emitChange();
+ },
+ _receiveUser(user) {
+ this.STATUS = 'DEFAULT';
+ this.user = user;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ user: this.user,
+ STATUS: this.STATUS
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.user = state.user;
+ this.STATUS = state.STATUS;
+ }
+});
diff --git a/app/scripts/stores/UserStore.js b/app/scripts/stores/UserStore.js
new file mode 100644
index 0000000000..ff68d1cfac
--- /dev/null
+++ b/app/scripts/stores/UserStore.js
@@ -0,0 +1,137 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import md5 from 'md5';
+var debug = require('debug')('stores: UserStore');
+
+var UserStore = createStore({
+ storeName: 'UserStore',
+ handlers: {
+ RECEIVE_USER: '_receiveUserFromHub',
+ RECEIVE_NAMESPACES: '_receiveNamespaces',
+ LOGOUT: '_logout',
+ EXPIRED_SIGNATURE: '_logout'
+ },
+ initialize: function() {
+ this.dateJoined = '';
+ this.fullName = '';
+ this.gravatarEmail = '';
+ this.gravatarUrl = '';
+ this.isActive = false;
+ this.isAdmin = false;
+ this.isStaff = false;
+ this.profileUrl = '';
+ this.company = '';
+ this.id = '';
+ this.location = '';
+ this.userType = 'User';
+ this.username = '';
+ this.namespaces = [];
+ },
+ _getGravatarUrl: function(email) {
+ return 'https://secure.gravatar.com/avatar/' + md5( email.trim().toLowerCase() );
+ },
+ _receiveUserFromHub: function(user) {
+
+ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
+ this.dateJoined = user.date_joined;
+ this.fullName = user.full_name;
+ this.gravatarEmail = user.gravatar_email;
+ //TODO: the url has to be handed off from the backend
+ //This fix should be temporary
+ this.gravatarUrl = (user.gravatar_url === user.gravatar_email) ?
+ this._getGravatarUrl(user.gravatar_email) : user.gravatar_url;
+ this.isActive = user.is_active;
+ this.isAdmin = user.is_admin;
+ this.isStaff = user.is_staff;
+ this.profileUrl = user.profile_url;
+ // jscs:enable
+
+ this.company = user.company;
+ this.id = user.id;
+ this.location = user.location;
+ this.userType = user.type;
+ this.username = user.username;
+
+ this.emitChange();
+ },
+ _receiveNamespaces: function(receivedNamespaces) {
+ //This is required for creating a repository
+ //Namespaces are attached to a user, due to permissions/access restrictions
+ //Eg. {
+ // "namespaces": [
+ // "user",
+ // "org1",
+ // "org2"
+ // ]
+ //}
+ this.namespaces = receivedNamespaces.namespaces;
+
+ this.emitChange();
+ },
+ _logout: function() {
+
+ this.company = '';
+ this.dateJoined = '';
+ this.fullName = '';
+ this.gravatarEmail = '';
+ this.gravatarUrl = '';
+ this.id = '';
+ this.isActive = false;
+ this.isAdmin = false;
+ this.isStaff = false;
+ this.location = '';
+ this.profileUrl = '';
+ this.userType = 'User';
+ this.username = '';
+ this.namespaces = [];
+
+ this.emitChange();
+ },
+ getState: function() {
+ return {
+ company: this.company,
+ dateJoined: this.dateJoined,
+ fullName: this.fullName,
+ gravatarEmail: this.gravatarEmail,
+ gravatarUrl: this.gravatarUrl,
+ id: this.id,
+ isActive: this.isActive,
+ isAdmin: this.isAdmin,
+ isStaff: this.isStaff,
+ location: this.location,
+ profileUrl: this.profileUrl,
+ userType: this.userType,
+ username: this.username,
+ namespaces: this.namespaces
+ };
+ },
+ getUsername: function() {
+ return this.username;
+ },
+ getNamespaces: function() {
+ return this.namespaces;
+ },
+ dehydrate: function() {
+ return this.getState();
+ },
+ rehydrate: function(state) {
+ debug('rehydrate', state);
+ this.dateJoined = state.dateJoined;
+ this.fullName = state.fullName;
+ this.gravatarEmail = state.gravatarEmail;
+ this.gravatarUrl = state.gravatarUrl;
+ this.isActive = state.isActive;
+ this.isAdmin = state.isAdmin;
+ this.isStaff = state.isStaff;
+ this.profileUrl = state.profileUrl;
+ this.company = state.company;
+ this.id = state.id;
+ this.location = state.location;
+ this.userType = state.userType;
+ this.username = state.username;
+ this.namespaces = state.namespaces;
+ }
+});
+
+module.exports = UserStore;
diff --git a/app/scripts/stores/WebhooksSettingsStore.js b/app/scripts/stores/WebhooksSettingsStore.js
new file mode 100644
index 0000000000..8c27103a95
--- /dev/null
+++ b/app/scripts/stores/WebhooksSettingsStore.js
@@ -0,0 +1,31 @@
+'use strict';
+import createStore from 'fluxible/addons/createStore';
+const debug = require('debug')('WebhooksSettingsStore');
+
+var WebhooksSettingsStore = createStore({
+ storeName: 'WebhooksSettingsStore',
+ handlers: {
+ RECEIVE_WEBHOOKS: '_receiveWebhooks'
+ },
+ initialize() {
+ this.pipelines = [];
+ },
+ _receiveWebhooks(payload) {
+ debug(payload);
+ this.pipelines = payload.results;
+ this.emitChange();
+ },
+ getState() {
+ return {
+ pipelines: this.pipelines
+ };
+ },
+ dehydrate() {
+ return this.getState();
+ },
+ rehydrate(state) {
+ this.pipelines = state.pipelines;
+ }
+});
+
+module.exports = WebhooksSettingsStore;
diff --git a/app/scripts/stores/addorganizationstore/Constants.js b/app/scripts/stores/addorganizationstore/Constants.js
new file mode 100644
index 0000000000..2da1465eb6
--- /dev/null
+++ b/app/scripts/stores/addorganizationstore/Constants.js
@@ -0,0 +1,15 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ BAD_REQUEST: null,
+ SUCCESSFUL: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/addtriallicensestore/Constants.js b/app/scripts/stores/addtriallicensestore/Constants.js
new file mode 100644
index 0000000000..08e9dfd0ff
--- /dev/null
+++ b/app/scripts/stores/addtriallicensestore/Constants.js
@@ -0,0 +1,11 @@
+'use strict';
+import keyMirror from 'keymirror';
+
+const STATUS = keyMirror({
+ ATTEMPTING_DOWNLOAD: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ SUCCESSFUL_DOWNLOAD: null
+});
+
+export default STATUS;
diff --git a/app/scripts/stores/addwebhookformstore/Constants.js b/app/scripts/stores/addwebhookformstore/Constants.js
new file mode 100644
index 0000000000..d2ed2ffbf4
--- /dev/null
+++ b/app/scripts/stores/addwebhookformstore/Constants.js
@@ -0,0 +1,10 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+export default keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ SUCCESSFUL: null,
+ ERROR: null
+});
diff --git a/app/scripts/stores/billingformstore/Constants.js b/app/scripts/stores/billingformstore/Constants.js
new file mode 100644
index 0000000000..42bb512ddf
--- /dev/null
+++ b/app/scripts/stores/billingformstore/Constants.js
@@ -0,0 +1,14 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ ATTEMPTING: null,
+ SUCCESS: null,
+ FORM_ERROR: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/collaborators/Constants.js b/app/scripts/stores/collaborators/Constants.js
new file mode 100644
index 0000000000..0092c8068c
--- /dev/null
+++ b/app/scripts/stores/collaborators/Constants.js
@@ -0,0 +1,14 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ ATTEMPTING: null,
+ SUCCESS: null,
+ ERROR: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/common/Constants.js b/app/scripts/stores/common/Constants.js
new file mode 100644
index 0000000000..63b6ce6890
--- /dev/null
+++ b/app/scripts/stores/common/Constants.js
@@ -0,0 +1,25 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+export const STATUS = keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ SUCCESSFUL: null,
+ ERROR: null
+});
+
+export const ACCOUNT = 'account';
+export const BILLING = 'billing';
+export const STRIPE_URL = 'https://api.stripe.com/v1/tokens';
+export const STRIPE_STAGE_TOKEN = 'pk_test_mNouiY3uYoBAfQYyTurrxf0Q';
+export const STRIPE_PROD_TOKEN = 'pk_live_89IjovLdwh2MTzV7JsGJK3qk';
+export const BF_STAGE_URL = 'https://api-sandbox.billforward.net:443/v1/tokenization/auth-capture';
+export const BF_PROD_URL = 'https://api.billforward.net/v1/tokenization/auth-capture';
+export const BF_STAGE_TOKEN = 'ec687f76-c1b6-4d71-b919-4fe99202ca13';
+export const BF_PROD_TOKEN = '650cbe35-4aca-4820-a7d1-accec8a7083a';
+export const BILLFORWARD_ACCOUNT_ID = 'billforward-account-id';
+export const v4BillingProfile = (docker_id) => {
+ return `/api/billing/v4/accounts/${docker_id}/profile`;
+};
diff --git a/app/scripts/stores/deletepipelinestore/Constants.js b/app/scripts/stores/deletepipelinestore/Constants.js
new file mode 100644
index 0000000000..c619b4befb
--- /dev/null
+++ b/app/scripts/stores/deletepipelinestore/Constants.js
@@ -0,0 +1,9 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+export default keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ SUCCESSFUL: null
+});
diff --git a/app/scripts/stores/deleterepostore/Constants.js b/app/scripts/stores/deleterepostore/Constants.js
new file mode 100644
index 0000000000..2282e4de76
--- /dev/null
+++ b/app/scripts/stores/deleterepostore/Constants.js
@@ -0,0 +1,14 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FORM_ERROR: null,
+ SHOWING_CONFIRM_BOX: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/emailsstore/Constants.js b/app/scripts/stores/emailsstore/Constants.js
new file mode 100644
index 0000000000..262d6665c5
--- /dev/null
+++ b/app/scripts/stores/emailsstore/Constants.js
@@ -0,0 +1,19 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ SAVING: null
+});
+
+var EMAILSTATUS = keyMirror({
+ SUCCESS: null,
+ ATTEMPTING: null,
+ FAILED: null
+});
+
+module.exports = {
+ STATUS,
+ EMAILSTATUS
+};
diff --git a/app/scripts/stores/enterprisetrialstore/Constants.js b/app/scripts/stores/enterprisetrialstore/Constants.js
new file mode 100644
index 0000000000..c1c53dce9c
--- /dev/null
+++ b/app/scripts/stores/enterprisetrialstore/Constants.js
@@ -0,0 +1,11 @@
+'use strict';
+import keyMirror from 'keymirror';
+
+const STATUS = keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FACEPALM: null,
+ SUCCESSFUL_SIGNUP: null
+});
+
+export default STATUS;
diff --git a/app/scripts/stores/enterprisetrialsuccessstore/Constants.js b/app/scripts/stores/enterprisetrialsuccessstore/Constants.js
new file mode 100644
index 0000000000..061a46f844
--- /dev/null
+++ b/app/scripts/stores/enterprisetrialsuccessstore/Constants.js
@@ -0,0 +1,9 @@
+'use strict';
+import keyMirror from 'keymirror';
+
+const STATUS = keyMirror({
+ DEFAULT: null,
+ ERROR: null
+});
+
+export default STATUS;
diff --git a/app/scripts/stores/loginstore/Constants.js b/app/scripts/stores/loginstore/Constants.js
new file mode 100644
index 0000000000..d8cc11d128
--- /dev/null
+++ b/app/scripts/stores/loginstore/Constants.js
@@ -0,0 +1,14 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ ATTEMPTING_LOGIN: null,
+ ERROR_UNAUTHORIZED: null,
+ GENERIC_ERROR: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/loginstore/createFormStore.js b/app/scripts/stores/loginstore/createFormStore.js
new file mode 100644
index 0000000000..cf17dd0cbe
--- /dev/null
+++ b/app/scripts/stores/loginstore/createFormStore.js
@@ -0,0 +1,88 @@
+'use strict';
+
+import createStore from 'fluxible/addons/createStore';
+import _ from 'lodash';
+var debug = require('debug')('createFormStore');
+/**
+ * @param {Array} fields - array of objects with field names
+ * as keys and inital values as values
+ * @param {Function} init - old initialize function
+ */
+
+export default function createFormStore(fields, oldSpec) {
+
+ var spec = {};
+
+ spec.initialize = function() {
+ this.globalFormError = '';
+ _.forOwn(fields, function(key, val){
+ this.fields[key] = {};
+ this.values[key] = val;
+ });
+ spec.initialize();
+ }.bind(spec);
+
+ spec._badRequest = function(obj) {
+ /**
+ * obj is an Object with keys that are field names
+ * and values that are arrays of errors
+ *
+ * This function should be used as the handler for
+ * an HTTP 400 BadRequest
+ *
+ * obj = {
+ * username: ["cannot be empty"]
+ * }
+ */
+ // did we update state?
+ var dirty = false;
+
+ _.forOwn(obj, function(key, val) {
+ if(_.includes(fields, key)) {
+ this.fields[key].hasError = !!val;
+ this.fields[key].error = val[0];
+ dirty = true;
+ }
+ }, this);
+
+ if(dirty) {
+ this.emitChange();
+ }
+ };
+
+ spec._getState = function() {
+ return {
+ fields: this.fields,
+ values: this.values,
+ globalFormError: this.globalFormError
+ };
+ };
+
+
+ spec._updateFieldWithValue = function({fieldKey, fieldValue}){
+ this.values[fieldKey] = fieldValue;
+ this.emitChange();
+ };
+
+
+ spec.dehydrate = function() {
+ return {};
+ },
+ spec.rehydrate = function(state) {
+ this.state = state;
+ };
+
+ _.merge(spec, oldSpec, function(objectValue, sourceValue, key) {
+ if(key === 'initializer') {
+ return sourceValue;
+ } else if(key === 'getState') {
+ return function() {
+ debug('state', this.state);
+ return _.merge({},
+ objectValue.getState(),
+ sourceValue._getState());
+ };
+ }
+ });
+ return createStore(spec);
+}
diff --git a/app/scripts/stores/orgteamstore/Constants.js b/app/scripts/stores/orgteamstore/Constants.js
new file mode 100644
index 0000000000..0731e53f0e
--- /dev/null
+++ b/app/scripts/stores/orgteamstore/Constants.js
@@ -0,0 +1,22 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ MEMBER_UNAUTHORIZED: null,
+ TEAM_UNAUTHORIZED: null,
+ MEMBER_ERROR: null,
+ TEAM_ERROR: null,
+ MEMBER_BAD_REQUEST: null,
+ TEAM_BAD_REQUEST: null,
+ GENERAL_SERVER_ERROR: null,
+ CREATE_TEAM_SUCCESS: null,
+ CREATE_MEMBER_SUCCESS: null,
+ UPDATE_TEAM_ERROR: null,
+ UPDATE_TEAM_SUCCESS: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/repodetailstags/Constants.js b/app/scripts/stores/repodetailstags/Constants.js
new file mode 100644
index 0000000000..42faf1dbcc
--- /dev/null
+++ b/app/scripts/stores/repodetailstags/Constants.js
@@ -0,0 +1,12 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ ERROR: null,
+ DELETING: null,
+ CONFIRMING: null
+});
+
+module.exports = STATUS;
diff --git a/app/scripts/stores/repostore/Constants.js b/app/scripts/stores/repostore/Constants.js
new file mode 100644
index 0000000000..f3faaad35f
--- /dev/null
+++ b/app/scripts/stores/repostore/Constants.js
@@ -0,0 +1,15 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ REPO_ALREADY_EXISTS: null,
+ PRIVATE_REPO_QUOTA_EXCEEDED: null,
+ BAD_REQUEST: null,
+ REPO_NOT_FOUND: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/repovisibilitystore/Constants.js b/app/scripts/stores/repovisibilitystore/Constants.js
new file mode 100644
index 0000000000..2282e4de76
--- /dev/null
+++ b/app/scripts/stores/repovisibilitystore/Constants.js
@@ -0,0 +1,14 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ ATTEMPTING: null,
+ DEFAULT: null,
+ FORM_ERROR: null,
+ SHOWING_CONFIRM_BOX: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/stores/signupstore/Constants.js b/app/scripts/stores/signupstore/Constants.js
new file mode 100644
index 0000000000..030ee671ac
--- /dev/null
+++ b/app/scripts/stores/signupstore/Constants.js
@@ -0,0 +1,13 @@
+'use strict';
+var keyMirror = require('keymirror');
+
+// Component-Global form states
+var STATUS = keyMirror({
+ DEFAULT: null,
+ ATTEMPTING_SIGNUP: null,
+ SUCCESSFUL_SIGNUP: null
+});
+
+module.exports = {
+ STATUS
+};
diff --git a/app/scripts/vendor/Blob.js b/app/scripts/vendor/Blob.js
new file mode 100644
index 0000000000..294debb35f
--- /dev/null
+++ b/app/scripts/vendor/Blob.js
@@ -0,0 +1,215 @@
+/* eslint-disable */
+/* Blob.js
+ * A Blob implementation.
+ * 2014-07-24
+ *
+ * By Eli Grey, http://eligrey.com
+ * By Devin Samarin, https://github.com/dsamarin
+ * License: X11/MIT
+ * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
+ */
+
+/*global self, unescape */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+ plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
+
+if(typeof window !== "undefined") {
+(function (view) {
+ "use strict";
+
+ view.URL = view.URL || view.webkitURL;
+
+ if (view.Blob && view.URL) {
+ try {
+ new Blob;
+ return;
+ } catch (e) {}
+ }
+
+ // Internally we use a BlobBuilder implementation to base Blob off of
+ // in order to support older browsers that only have BlobBuilder
+ var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
+ var
+ get_class = function(object) {
+ return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
+ }
+ , FakeBlobBuilder = function BlobBuilder() {
+ this.data = [];
+ }
+ , FakeBlob = function Blob(data, type, encoding) {
+ this.data = data;
+ this.size = data.length;
+ this.type = type;
+ this.encoding = encoding;
+ }
+ , FBB_proto = FakeBlobBuilder.prototype
+ , FB_proto = FakeBlob.prototype
+ , FileReaderSync = view.FileReaderSync
+ , FileException = function(type) {
+ this.code = this[this.name = type];
+ }
+ , file_ex_codes = (
+ "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
+ ).split(" ")
+ , file_ex_code = file_ex_codes.length
+ , real_URL = view.URL || view.webkitURL || view
+ , real_create_object_URL = real_URL.createObjectURL
+ , real_revoke_object_URL = real_URL.revokeObjectURL
+ , URL = real_URL
+ , btoa = view.btoa
+ , atob = view.atob
+
+ , ArrayBuffer = view.ArrayBuffer
+ , Uint8Array = view.Uint8Array
+
+ , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
+ ;
+ FakeBlob.fake = FB_proto.fake = true;
+ while (file_ex_code--) {
+ FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
+ }
+ // Polyfill URL
+ if (!real_URL.createObjectURL) {
+ URL = view.URL = function(uri) {
+ var
+ uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
+ , uri_origin
+ ;
+ uri_info.href = uri;
+ if (!("origin" in uri_info)) {
+ if (uri_info.protocol.toLowerCase() === "data:") {
+ uri_info.origin = null;
+ } else {
+ uri_origin = uri.match(origin);
+ uri_info.origin = uri_origin && uri_origin[1];
+ }
+ }
+ return uri_info;
+ };
+ }
+ URL.createObjectURL = function(blob) {
+ var
+ type = blob.type
+ , data_URI_header
+ ;
+ if (type === null) {
+ type = "application/octet-stream";
+ }
+ if (blob instanceof FakeBlob) {
+ data_URI_header = "data:" + type;
+ if (blob.encoding === "base64") {
+ return data_URI_header + ";base64," + blob.data;
+ } else if (blob.encoding === "URI") {
+ return data_URI_header + "," + decodeURIComponent(blob.data);
+ } if (btoa) {
+ return data_URI_header + ";base64," + btoa(blob.data);
+ } else {
+ return data_URI_header + "," + encodeURIComponent(blob.data);
+ }
+ } else if (real_create_object_URL) {
+ return real_create_object_URL.call(real_URL, blob);
+ }
+ };
+ URL.revokeObjectURL = function(object_URL) {
+ if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
+ real_revoke_object_URL.call(real_URL, object_URL);
+ }
+ };
+ FBB_proto.append = function(data/*, endings*/) {
+ var bb = this.data;
+ // decode data to a binary string
+ if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
+ var
+ str = ""
+ , buf = new Uint8Array(data)
+ , i = 0
+ , buf_len = buf.length
+ ;
+ for (; i < buf_len; i++) {
+ str += String.fromCharCode(buf[i]);
+ }
+ bb.push(str);
+ } else if (get_class(data) === "Blob" || get_class(data) === "File") {
+ if (FileReaderSync) {
+ var fr = new FileReaderSync;
+ bb.push(fr.readAsBinaryString(data));
+ } else {
+ // async FileReader won't work as BlobBuilder is sync
+ throw new FileException("NOT_READABLE_ERR");
+ }
+ } else if (data instanceof FakeBlob) {
+ if (data.encoding === "base64" && atob) {
+ bb.push(atob(data.data));
+ } else if (data.encoding === "URI") {
+ bb.push(decodeURIComponent(data.data));
+ } else if (data.encoding === "raw") {
+ bb.push(data.data);
+ }
+ } else {
+ if (typeof data !== "string") {
+ data += ""; // convert unsupported types to strings
+ }
+ // decode UTF-16 to binary string
+ bb.push(unescape(encodeURIComponent(data)));
+ }
+ };
+ FBB_proto.getBlob = function(type) {
+ if (!arguments.length) {
+ type = null;
+ }
+ return new FakeBlob(this.data.join(""), type, "raw");
+ };
+ FBB_proto.toString = function() {
+ return "[object BlobBuilder]";
+ };
+ FB_proto.slice = function(start, end, type) {
+ var args = arguments.length;
+ if (args < 3) {
+ type = null;
+ }
+ return new FakeBlob(
+ this.data.slice(start, args > 1 ? end : this.data.length)
+ , type
+ , this.encoding
+ );
+ };
+ FB_proto.toString = function() {
+ return "[object Blob]";
+ };
+ FB_proto.close = function() {
+ this.size = 0;
+ delete this.data;
+ };
+ return FakeBlobBuilder;
+ }(view));
+
+ view.Blob = function(blobParts, options) {
+ var type = options ? (options.type || "") : "";
+ var builder = new BlobBuilder();
+ if (blobParts) {
+ for (var i = 0, len = blobParts.length; i < len; i++) {
+ if (Uint8Array && blobParts[i] instanceof Uint8Array) {
+ builder.append(blobParts[i].buffer);
+ }
+ else {
+ builder.append(blobParts[i]);
+ }
+ }
+ }
+ var blob = builder.getBlob(type);
+ if (!blob.slice && blob.webkitSlice) {
+ blob.slice = blob.webkitSlice;
+ }
+ return blob;
+ };
+
+ var getPrototypeOf = Object.getPrototypeOf || function(object) {
+ return object.__proto__;
+ };
+ view.Blob.prototype = getPrototypeOf(new view.Blob());
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
+}
+ /* eslint-enable */
diff --git a/app/scripts/vendor/FileSaver.js b/app/scripts/vendor/FileSaver.js
new file mode 100644
index 0000000000..910a217e1e
--- /dev/null
+++ b/app/scripts/vendor/FileSaver.js
@@ -0,0 +1,260 @@
+/* eslint-disable */
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 2015-05-07.2
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+if(typeof window !== "undefined") {
+
+var saveAs = saveAs || (function(view) {
+ "use strict";
+ // IE <10 is explicitly unsupported
+ if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
+ return;
+ }
+ var
+ doc = view.document
+ // only get URL when necessary in case Blob.js hasn't overridden it yet
+ , get_URL = function() {
+ return view.URL || view.webkitURL || view;
+ }
+ , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+ , can_use_save_link = "download" in save_link
+ , click = function(node) {
+ var event = doc.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", true, false, view, 0, 0, 0, 0, 0
+ , false, false, false, false, 0, null
+ );
+ node.dispatchEvent(event);
+ }
+ , webkit_req_fs = view.webkitRequestFileSystem
+ , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+ , throw_outside = function(ex) {
+ (view.setImmediate || view.setTimeout)(function() {
+ throw ex;
+ }, 0);
+ }
+ , force_saveable_type = "application/octet-stream"
+ , fs_min_size = 0
+ // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
+ // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
+ // for the reasoning behind the timeout and revocation flow
+ , arbitrary_revoke_timeout = 500 // in ms
+ , revoke = function(file) {
+ var revoker = function() {
+ if (typeof file === "string") { // file is an object URL
+ get_URL().revokeObjectURL(file);
+ } else { // file is a File
+ file.remove();
+ }
+ };
+ if (view.chrome) {
+ revoker();
+ } else {
+ setTimeout(revoker, arbitrary_revoke_timeout);
+ }
+ }
+ , dispatch = function(filesaver, event_types, event) {
+ event_types = [].concat(event_types);
+ var i = event_types.length;
+ while (i--) {
+ var listener = filesaver["on" + event_types[i]];
+ if (typeof listener === "function") {
+ try {
+ listener.call(filesaver, event || filesaver);
+ } catch (ex) {
+ throw_outside(ex);
+ }
+ }
+ }
+ }
+ , auto_bom = function(blob) {
+ // prepend BOM for UTF-8 XML and text/* types (including HTML)
+ if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
+ return new Blob(["\ufeff", blob], {type: blob.type});
+ }
+ return blob;
+ }
+ , FileSaver = function(blob, name) {
+ blob = auto_bom(blob);
+ // First try a.download, then web filesystem, then object URLs
+ var
+ filesaver = this
+ , type = blob.type
+ , blob_changed = false
+ , object_url
+ , target_view
+ , dispatch_all = function() {
+ dispatch(filesaver, "writestart progress write writeend".split(" "));
+ }
+ // on any filesys errors revert to saving with object URLs
+ , fs_error = function() {
+ // don't create more object URLs than needed
+ if (blob_changed || !object_url) {
+ object_url = get_URL().createObjectURL(blob);
+ }
+ if (target_view) {
+ target_view.location.href = object_url;
+ } else {
+ var new_tab = view.open(object_url, "_blank");
+ if (new_tab == undefined && typeof safari !== "undefined") {
+ //Apple do not allow window.open, see http://bit.ly/1kZffRI
+ view.location.href = object_url
+ }
+ }
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ revoke(object_url);
+ }
+ , abortable = function(func) {
+ return function() {
+ if (filesaver.readyState !== filesaver.DONE) {
+ return func.apply(this, arguments);
+ }
+ };
+ }
+ , create_if_not_found = {create: true, exclusive: false}
+ , slice
+ ;
+ filesaver.readyState = filesaver.INIT;
+ if (!name) {
+ name = "download";
+ }
+ if (can_use_save_link) {
+ object_url = get_URL().createObjectURL(blob);
+ save_link.href = object_url;
+ save_link.download = name;
+ click(save_link);
+ filesaver.readyState = filesaver.DONE;
+ dispatch_all();
+ revoke(object_url);
+ return;
+ }
+ // Object and web filesystem URLs have a problem saving in Google Chrome when
+ // viewed in a tab, so I force save with application/octet-stream
+ // http://code.google.com/p/chromium/issues/detail?id=91158
+ // Update: Google errantly closed 91158, I submitted it again:
+ // https://code.google.com/p/chromium/issues/detail?id=389642
+ if (view.chrome && type && type !== force_saveable_type) {
+ slice = blob.slice || blob.webkitSlice;
+ blob = slice.call(blob, 0, blob.size, force_saveable_type);
+ blob_changed = true;
+ }
+ // Since I can't be sure that the guessed media type will trigger a download
+ // in WebKit, I append .download to the filename.
+ // https://bugs.webkit.org/show_bug.cgi?id=65440
+ if (webkit_req_fs && name !== "download") {
+ name += ".download";
+ }
+ if (type === force_saveable_type || webkit_req_fs) {
+ target_view = view;
+ }
+ if (!req_fs) {
+ fs_error();
+ return;
+ }
+ fs_min_size += blob.size;
+ req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+ fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+ var save = function() {
+ dir.getFile(name, create_if_not_found, abortable(function(file) {
+ file.createWriter(abortable(function(writer) {
+ writer.onwriteend = function(event) {
+ target_view.location.href = file.toURL();
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "writeend", event);
+ revoke(file);
+ };
+ writer.onerror = function() {
+ var error = writer.error;
+ if (error.code !== error.ABORT_ERR) {
+ fs_error();
+ }
+ };
+ "writestart progress write abort".split(" ").forEach(function(event) {
+ writer["on" + event] = filesaver["on" + event];
+ });
+ writer.write(blob);
+ filesaver.abort = function() {
+ writer.abort();
+ filesaver.readyState = filesaver.DONE;
+ };
+ filesaver.readyState = filesaver.WRITING;
+ }), fs_error);
+ }), fs_error);
+ };
+ dir.getFile(name, {create: false}, abortable(function(file) {
+ // delete file if it already exists
+ file.remove();
+ save();
+ }), abortable(function(ex) {
+ if (ex.code === ex.NOT_FOUND_ERR) {
+ save();
+ } else {
+ fs_error();
+ }
+ }));
+ }), fs_error);
+ }), fs_error);
+ }
+ , FS_proto = FileSaver.prototype
+ , saveAs = function(blob, name) {
+ return new FileSaver(blob, name);
+ }
+ ;
+ // IE 10+ (native saveAs)
+ if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
+ return function(blob, name) {
+ return navigator.msSaveOrOpenBlob(auto_bom(blob), name);
+ };
+ }
+
+ FS_proto.abort = function() {
+ var filesaver = this;
+ filesaver.readyState = filesaver.DONE;
+ dispatch(filesaver, "abort");
+ };
+ FS_proto.readyState = FS_proto.INIT = 0;
+ FS_proto.WRITING = 1;
+ FS_proto.DONE = 2;
+
+ FS_proto.error =
+ FS_proto.onwritestart =
+ FS_proto.onprogress =
+ FS_proto.onwrite =
+ FS_proto.onabort =
+ FS_proto.onerror =
+ FS_proto.onwriteend =
+ null;
+
+ return saveAs;
+}(
+ typeof self !== "undefined" && self
+ || typeof window !== "undefined" && window
+ || this && this.content
+));
+// `self` is undefined in Firefox for Android content script context
+// while `this` is nsIContentFrameMessageManager
+// with an attribute `content` that corresponds to the window
+
+if (typeof module !== "undefined" && module.exports) {
+ module.exports.saveAs = saveAs;
+} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
+ define([], function() {
+ return saveAs;
+ });
+}
+} else {
+ // nope
+}
+/* eslint-enable */
diff --git a/app/styles/common/_repository-list-item.scss b/app/styles/common/_repository-list-item.scss
new file mode 100644
index 0000000000..1cd1411c48
--- /dev/null
+++ b/app/styles/common/_repository-list-item.scss
@@ -0,0 +1,98 @@
+.repository-list-item {
+ display: flex;
+ flex-flow: row;
+ border: 1px solid $secondary-5;
+ border-radius: $global-radius;
+ margin: 1rem;
+ font-weight: $font-weight-bold;
+ color: $secondary-2;
+ background: $white;
+ &:hover {
+ border-color: $primary-1;
+ .section {
+ &.action {
+ background-color: $primary-1;
+ color: white;
+ border-color: $primary-1;
+ }
+ }
+ }
+ > a {
+ display: flex;
+ flex: 1;
+ }
+ .avatar {
+ background: $primary-color;
+ width: 50px;
+ height: 50px;
+ border-radius: $global-radius;
+ }
+ .section {
+ display: flex;
+ border-left: 1px solid $secondary-5;
+ padding: 1rem;
+ flex-flow: row;
+ &.head {
+ flex-grow: 1;
+ }
+ &:first-child {
+ border-left: 0;
+ }
+ &.stats {
+ min-width: 100px;
+ text-align: center;
+ }
+ &.action {
+ background-color: $secondary-5;;
+ color: $secondary-4;
+ flex-flow: column;
+ i {
+ position: relative;
+ top: 0.5rem;
+ left: 1.2rem;
+ }
+ .text {
+ margin-top: 1.2rem;
+ margin-left: 0.4rem;
+ font-size: 0.7rem;
+ }
+ }
+ .title {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.25rem;
+ }
+ .labels {
+ font-size: rem-calc(12px);
+ color: $secondary-4;
+ .official {
+ color: $primary-1;
+ }
+ .public {
+ color: darken($primary-1, 30%);
+ }
+ .private {
+ color: darken($primary-6, 35%);
+ }
+ .automated {
+ color: $secondary-4;
+ }
+ }
+ .label-value {
+ padding-top: 0.25rem;
+ margin: 0 auto;
+ .value {
+ font-size: rem-calc(14px);
+ margin-bottom: 0.25rem;
+ color: $secondary-3;
+ }
+ .sub-label {
+ font-size: rem-calc(10px);
+ color: $secondary-3;
+ }
+ }
+ .repo-name {
+ font-size: rem-calc(18px);
+ }
+ }
+}
diff --git a/app/styles/font-awesome.min.css b/app/styles/font-awesome.min.css
new file mode 100644
index 0000000000..9c24364d62
--- /dev/null
+++ b/app/styles/font-awesome.min.css
@@ -0,0 +1,4 @@
+/*!
+ * Font Awesome 4.4.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.4.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.4.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.4.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.4.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.4.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.4.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}
\ No newline at end of file
diff --git a/app/styles/hub.scss b/app/styles/hub.scss
new file mode 100644
index 0000000000..3ec7183cf9
--- /dev/null
+++ b/app/styles/hub.scss
@@ -0,0 +1,18 @@
+@import 'hub/account';
+@import 'hub/autobuilds';
+@import 'hub/badge';
+@import 'hub/blankslate';
+@import 'hub/flyout-menu';
+@import 'hub/react-tagsinput';
+@import 'hub/main-nav';
+@import 'hub/profile';
+@import 'hub/repositories';
+@import 'hub/repository-settings';
+@import 'hub/reset-password';
+@import 'hub/search';
+@import 'hub/tabs';
+@import 'hub/type';
+@import 'hub/usericons';
+@import 'hub/welcome';
+@import 'hub/forms';
+@import 'hub/docker-trusted-registry';
diff --git a/app/styles/hub/_blankslate.scss b/app/styles/hub/_blankslate.scss
new file mode 100644
index 0000000000..7fb8a37d73
--- /dev/null
+++ b/app/styles/hub/_blankslate.scss
@@ -0,0 +1,84 @@
+/*
+ * For pages without content
+ *
+ */
+
+.blankslate {
+ text-align: center;
+ background: #fff;
+ border-bottom: 1px solid #cbd1d7;
+ padding: 2.4rem;
+ h1 {
+ font-size: 2.5em;
+ }
+ .fa {
+ color: #566471;
+ font-size: 4em;
+ margin-bottom: 15px;
+ }
+}
+
+.blankslate-alt {
+ text-align: center;
+ padding: 5rem;
+}
+
+/*
+ *
+ * buttons : TODO: MOVE TO FOUNDATION OVERRIDES & SPLIT OUT THESE RANDOM BUTTONS
+ *
+ */
+
+button,
+.button,
+.profile-settings .change-pass-form .change-pass-save .button,
+.settings-wrapper input[type='submit'] {
+ -moz-appearance: none;
+ border-radius: $global-radius;
+ border-color: #1298c6;
+ border-style: solid;
+ border-width: 0;
+ color: #ffffff;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: normal;
+ margin: 0 0 1.25rem;
+ padding: 0.7rem 1rem 0.7rem 0.9rem;
+ text-align: center;
+ transition: background-color 300ms ease-out 0s;
+ box-shadow: none;
+ &.default {
+ background-color: #fff;
+ border: 1px solid #dfe4ea;
+ color: #000;
+ }
+ &.primary {
+ background: $primary-1;
+ border-radius: $global-radius;
+ color: #fff;
+ &:hover {
+ background: $primary-2;
+ }
+ }
+ // &.secondary {/** will switch this with .default **/}
+ &.dashed {
+ background-color: transparent;
+ border: 1px dashed #c3ccd9;
+ color: #2f4559;/** need variable **/
+ border-radius: $global-radius;
+ &:hover {
+ opacity: .8;
+ }
+ }
+ &.button.disabled {
+ cursor: not-allowed;
+ }
+ &.small {
+ padding: 0.7rem 1rem 0.7rem 0.9rem;
+ }
+ &.xl {
+ padding: 1rem 2rem 1rem 2rem;
+ margin-right: 1rem;
+ }
+}
diff --git a/app/styles/hub/_profile.scss b/app/styles/hub/_profile.scss
new file mode 100644
index 0000000000..cca73f4675
--- /dev/null
+++ b/app/styles/hub/_profile.scss
@@ -0,0 +1,30 @@
+.gravatar {
+ margin: 1.5rem 0;
+ img {
+ border: 1px solid $secondary-5;
+ border-radius: $global-radius;
+ }
+}
+
+.profile-info {
+ padding: .5rem 1rem 0 1rem;
+ ul {
+ padding: .6rem 0;
+ li {
+ padding: .2rem 0;
+ color: $secondary-4;
+ .fa {
+ margin: 0 .5rem 0 0;
+ min-width: .8rem;
+ text-align: center;
+ }
+ }
+ }
+}
+
+.profile-repos {
+ padding: 1rem 0;
+ h4 {
+ padding-left: 2rem;
+ }
+}
diff --git a/app/styles/hub/_type.scss b/app/styles/hub/_type.scss
new file mode 100644
index 0000000000..683ab8a3cd
--- /dev/null
+++ b/app/styles/hub/_type.scss
@@ -0,0 +1,19 @@
+/*
+ * This file is in progress - will contain global typography styles
+ *
+ */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: $primary-1;
+ font-weight: 200;
+}
+h1 {
+ font-size: 1.8875rem;
+}
+h2 {
+ font-size: 1.3875rem;
+}
\ No newline at end of file
diff --git a/app/styles/hub/account.scss b/app/styles/hub/account.scss
new file mode 100644
index 0000000000..5d577764e4
--- /dev/null
+++ b/app/styles/hub/account.scss
@@ -0,0 +1,518 @@
+.settings-wrapper {
+ width: 100%;
+ input {
+ &[type='text'],&[type='email'],&[type='password'] {
+ color: $secondary-1;
+ }
+ &[type='submit'] {
+ padding: .5rem;
+ }
+ }
+ .settings-body {
+ padding: 25px 0px;
+ .account-section-text {
+ float: left;
+ display:inline-block;
+ }
+ .account-section-header {
+ padding-left: 0;
+ .account-section-title{
+ color: $secondary-1;
+ font-weight: $font-weight-bold;
+ }
+ .account-section-subtitle{
+ color: $primary-2;
+ margin-bottom: .5rem;
+ }
+ }
+ .account-section {
+ margin-bottom: 2rem;
+ .columns:last-child {
+ float: left;
+ }
+ }
+ }
+}
+
+.profile-settings {
+ .default-repo-visibility {
+ padding: 1rem;
+ .visibility-toggle {
+ &:hover {
+ cursor: pointer;
+ cursor: hand;
+ }
+ input {
+ margin-right: .5rem;
+ }
+ }
+ }
+ .email-wrapper {
+ border: 1px solid $border-input;
+ background: white;
+ form {
+ .add-email {
+ padding: 2rem 1rem 0;
+ .email-title {
+ font-weight: $font-weight-bold;
+ }
+ }
+ .email-element {
+ height: 4rem;
+ padding: 0 1rem;
+ margin: .5rem .5rem;
+ display: flex;
+ align-items: left;
+ .emphasis {/** TODO: Scope to .email-element for the moment, however will mostly be global **/
+ color: $secondary-3;
+ font-weight: 800;
+ }
+ .fa-times {/** TODO: Scope to .email-element for the moment, however will mostly be global **/
+ color: $primary-5;
+ font-size: 1.3rem;
+ &:hover {
+ color: $secondary-1;
+ }
+ }
+ .close-button {/** TODO: Let's verify we still need this **/
+ color: white;
+ display: inline-block;
+ background: $docker-dark--placeholders;
+ border-radius: 100%;
+ width: 1.5rem;
+ height: 1.5rem;
+ text-align: center;
+ margin-right: .5rem;
+ float: right;
+ &:hover {
+ background: darken($docker-dark--placeholders, 10%);
+ }
+ }
+ .email-action {
+ text-align: center;
+ border-radius: 1rem;
+ &.status-sent {
+ border: 1px solid $primary-2;
+ color: $primary-2;
+ }
+ &.status-sending {
+ border: 1px solid $primary-1;
+ color: $primary-1;
+ }
+ &.status-failed {
+ border: 1px solid $primary-5;
+ color: $primary-5;
+ }
+ }
+ .email-address {
+ word-wrap: break-word;
+ }
+ }
+ }
+ }
+ .change-pass-form {
+ border: 1px solid $border-input;
+ background: white;
+ padding: 3rem 3rem 1rem 3rem;
+ .change-pass-save {
+ height: 250px;
+ display: flex;
+ align-items: flex-end;
+ .button {
+ padding:.5rem;
+ }
+ }
+ &.success {
+ border: 1px solid $primary-2;
+ }
+ &.form-error {
+ border: 1px solid $primary-5;
+ }
+ }
+ .account-info-wrapper {
+ border: 1px solid $border-input;
+ background: white;
+ padding: 3rem 3rem 1rem;
+ .button {
+ float: right;
+ }
+ &.success {
+ border: 1px solid $primary-2;
+ }
+ &.form-error {
+ border: 1px solid $primary-5;
+ }
+ }
+ .toOrgButton {
+ padding: .5rem 0;
+ }
+}
+.convert-to-org{
+ .toOrg-title {
+ font-weight: $font-weight-bold;
+ }
+ .toOrg-body {
+ margin-bottom: 1rem;
+ }
+ .warning {
+ text-align: center;
+ color: $primary-5;
+ }
+ .convert-org-form {
+ padding: 3rem 2rem .5rem;
+ background: white;
+ border: 1px solid $border-input;
+ .button {
+ padding:.5rem;
+ &:disabled {
+ background: $secondary-5;
+ }
+ }
+ }
+}
+
+.accounts-services {
+ .linked-accounts {
+ margin-bottom: 5rem;
+ .link-service {
+ padding: 1.25rem;
+ margin: 0 1rem;
+ height: 13rem;
+ border: 1px solid $border-input;
+ background: white;
+ float: left;
+ .service-title {
+ display:flex;
+ align-items: center;
+ .linked-icon {
+ width: 100%;
+ height: auto;
+ }
+ .service-name {
+ font-weight: $font-weight-bold;
+ margin: 0 1rem;
+ }
+ }
+ .user-access {
+ margin: .5rem 0 0;
+ }
+ .button {
+ margin: 1rem 0 0;
+ float: right;
+ }
+ }
+ }
+}
+
+.account-notifications {
+ form {
+ background: white;
+ border: 1px solid $border-input;
+ padding: 2rem 2rem 1rem;
+ .button {
+ margin-right: 1rem;
+ // padding: .5rem;
+ float: right;
+ }
+ &.success {
+ border: 1px solid $primary-2;
+ }
+ &.form-error {
+ border: 1px solid $primary-5;
+ }
+ }
+ .event-notifs {
+ margin-bottom: 5rem;
+ }
+ .notification {
+ margin-bottom: 1rem;
+ &:hover {
+ cursor: pointer;
+ cursor: hand;
+ }
+ &.unverified {
+ color: $secondary-4;
+ cursor: default;
+ }
+ }
+}
+
+.billing-plans {
+ .columns:last-child {
+ float: left;
+ }
+ .account-billing-info {
+ margin-left: .5rem;
+ }
+ .plans-error-message {
+ background: $alert-color;
+ color:white;
+ }
+ .plans-q {
+ .q-title {
+ font-weight: 500;
+ color: $primary-2;
+ }
+ .q-answer {
+ padding-left: 1rem;
+ }
+ margin-bottom: 2rem;
+ }
+ .billing-info {
+ background: white;
+ border: 1px solid $border-input;
+ padding: 2rem 0rem 2rem 2rem;
+ margin: .5rem;
+ .row {
+ margin-bottom: 1rem;
+ .info-content {
+ border-left: 1px solid $border-input;
+ padding: .5rem 3rem;
+ }
+ .no-account {
+ text-align: center;
+ }
+ }
+ }
+ .billing-form-wrapper {
+ border: 1px solid $border-input;
+ background: white;
+ padding: 3rem 3rem 1rem;
+ .billing-form-header {
+ text-align: center;
+ }
+ .billing-info-form {
+ .billing-form-section {
+ margin-bottom: 1.5rem;
+ }
+ .billing-field * {
+ width: 100%;
+ }
+ .billing-dropdown {
+ width: 100%;
+ &.error {
+ border: 1px solid $alert-color;
+ }
+ }
+ .accepted-cards {
+ margin-bottom: 35px;
+ .card-icon {
+ font-size: 1.5rem;
+ margin-left: .5rem;
+ }
+ }
+ .above-selects .group {
+ margin-bottom: 15px;
+ }
+ .selects {
+ .date-text {
+ text-align: center;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ }
+ .columns {
+ margin-bottom: 20px;
+ }
+ }
+ .back {
+ margin-left: 2rem;
+ }
+ }
+ }
+ .preview-box {
+ border: 1px solid $border-input;
+ background: white;
+ padding: 2rem 2rem 0;
+ margin-left: 1rem;
+ .total {
+ margin-bottom: 35px;
+ }
+ .coupon_code * {
+ width: 100%;
+ }
+ .price {
+ text-align: right;
+ }
+ }
+ .invoice-table {
+ a.not-active {
+ pointer-events: none;
+ cursor: default;
+ color: $secondary-5;
+ }
+ }
+}
+.orgs-body {
+ padding-top: rem-calc(15px);
+ padding-bottom: rem-calc(20px);
+}
+
+.orgs-grid {
+ margin-left: 1.6rem;
+}
+
+.orgs-settings {
+ h5 {
+ font-weight: 400;
+ color: #22b8eb;/** find varible and update */
+ float: left;
+ margin: 0 0 0.8rem 1rem;
+ // color: $docker-dark;
+ }
+ .page-header-buttons {
+ float: left;
+ padding-left: 20px;
+ button {
+ margin-right: rem-calc(10px);
+ }
+ }
+ .new-org-form {
+ width: 100%;
+ }
+ .org-details {
+ ul {
+ list-style: none;
+ margin-left: 0;
+ li {
+ background-color: $panel-bg;
+ padding: rem-calc(10px);
+ margin-bottom: rem-calc(5px);
+ border-radius: rem-calc(2px);
+ margin: 0 .2rem 0 0;
+ }
+ }
+ .org-team-md {
+ margin-left: rem-calc(-15px);
+ .inline-list {
+ margin: 0;
+ }
+ h5 {
+ margin-left: rem-calc(10px);
+ font-weight: 300;
+ font-color: #22b8eb;
+ }
+ h6 {
+ background-color: lighten($panel-bg, 8%);
+ border-radius: $global-radius;
+ padding: 10px;
+ .action-icon {
+ background-color: lighten($panel-bg, 8%);
+ }
+ }
+ .org-members {
+ li {
+ img {
+ margin-right: rem-calc(10px);
+ border-radius: 100%;
+ }
+ width: rem-calc(200px);
+ padding: rem-calc(12px);
+ margin-right: rem-calc(20px);
+ }
+ }
+ // .org-teams {}
+ }
+ /** hide border when dux-form is in tabs **/
+ .dux-form {
+ border: 0;
+ }
+ }
+ .tab-title {
+ border: 1px solid #c4cdda;
+ border-bottom: none;
+ border-top-left-radius: $global-radius;
+ border-top-right-radius: $global-radius;
+ background-color: #e6edf4;
+ padding: 0.5rem;
+ &:hover {
+ cursor: pointer;
+ color: #23b8eb;
+ }
+ }
+ .tab-title.active {
+ background-color: white;
+ }
+ .tabs-content {
+ border-bottom-left-radius: $global-radius;
+ border-bottom-right-radius: $global-radius;
+ border-top-right-radius: $global-radius;
+ border: 1px solid #c4cdda;
+ background-color: white;
+ padding: rem-calc(20px);
+ }
+}
+
+//Teams
+.team-item {
+ a {
+ margin-left: rem-calc(15px);
+ }
+}
+
+.add-team-container {
+ form {
+ margin-top: 1.6rem;
+ .alert-box {
+ margin-left: 0.5rem !important;
+ margin-right: 0.5rem !important;
+ }
+ }
+}
+
+.add-team-button-group {
+ input[type="submit"], input[type="reset"] {
+ border-radius: $global-radius;
+ margin-left: 1rem;
+ }
+}
+
+//global
+li.li-no-hover {
+ &:hover {
+ background-color: transparent !important;
+ }
+}
+
+.delete-member-item {
+ height: rem-calc(25px);
+ cursor: default;
+ a {
+ float: left;
+ margin-left: 0.7rem;
+ }
+ i {
+ margin-top: 0.3rem;
+ margin-right: 1.4rem;
+ cursor: pointer;
+ color: #FF5151;
+ }
+}
+.add-member-item {
+ height: rem-calc(25px);
+ cursor: text;
+ input[type="text"] {
+ font-size: rem-calc(16px);
+ margin-left: 0.5rem;
+ float: left;
+ border: 0;
+ height: 2rem;
+ line-height: 2rem;
+ color: $gray-2;
+ width: rem-calc(500px);
+ &:hover {
+ color: $gray-2;
+ }
+ &:active {
+ color: $gray-2;
+ }
+ &:focus {
+ color: $gray-2;
+ }
+ }
+ i {
+ margin-top: 0.3rem;
+ margin-right: -1.5rem;
+ cursor: pointer;
+ }
+}
diff --git a/app/styles/hub/autobuilds.scss b/app/styles/hub/autobuilds.scss
new file mode 100644
index 0000000000..f95ba83e46
--- /dev/null
+++ b/app/styles/hub/autobuilds.scss
@@ -0,0 +1,108 @@
+.ol-decimal {
+ list-style-type: decimal;
+}
+
+.auto-build-tags-table {
+ input[type="text"] {
+ max-width:180px;
+ }
+}
+
+.autobuild-checkbox {
+ font-size: 14px;
+ font-weight: 300;
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ .checkbox-text {
+ margin-left: 5px;
+ }
+}
+
+//GLOBAL?
+.two-level-selector {
+ margin-top: 1.0rem;
+ .dux-form {
+ padding: 0;
+ margin: 0;
+ }
+ .row {
+ padding: 0;
+ margin: 0;
+ .columns {
+ padding: 0;
+ }
+ }
+ //this is actually a lighter filter bar
+ .searchbar input[type=text] {
+ background-color: $secondary-4;
+ width: 100%;
+ }
+ .filterbar-container {
+ margin-left: rem-calc(24px);
+ }
+}
+
+//Strip out the module class
+ul.stripped-module {
+ background: #fff;
+ list-style: none;
+ padding: 0 !important;
+ margin: 0 !important;
+ li {
+ text-align: left;
+ border-bottom: 1px solid #cbd1d7;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ &:hover {
+ background: $secondary-6;
+ cursor: pointer;
+ }
+ .list-item {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+ }
+ img {
+ border-radius: $global-radius;
+ }
+ .header-bar {
+ background-color: $secondary-6;
+ min-height: rem-calc(60px);
+ cursor: default;
+ }
+ .header-title {
+ margin-left: 1rem;
+ margin-top: 0.5rem;
+ font-size: rem-calc(18px);
+ font-weight: 400;
+ }
+ .alert-box {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ }
+}
+
+ul.left-border {
+ border-left: 1px solid #cbd1d7;
+}
+
+ul.right-border {
+ border-right: 1px solid #cbd1d7;
+}
+
+//fa i class needs this
+.arrow-sel {
+ margin-top: 0.25rem;
+ margin-right: 0.25rem;
+}
+
+.create-autobuild-form {
+ .form-error {
+ border: 1px solid $primary-5;
+ }
+ .text-error {
+ color: $primary-5;
+ }
+}
diff --git a/app/styles/hub/badge.scss b/app/styles/hub/badge.scss
new file mode 100644
index 0000000000..f17d6adac9
--- /dev/null
+++ b/app/styles/hub/badge.scss
@@ -0,0 +1,11 @@
+.dux-badge {
+ border-radius: rem-calc(4px);
+ padding: rem-calc(3px);
+ &.official {
+ color: #23b8eb;
+ }
+ &.builds {
+ color: #86d800;
+ font-size: rem-calc(12px);
+ }
+}
diff --git a/app/styles/hub/flyout-menu.scss b/app/styles/hub/flyout-menu.scss
new file mode 100644
index 0000000000..e8234f592f
--- /dev/null
+++ b/app/styles/hub/flyout-menu.scss
@@ -0,0 +1,185 @@
+//@extend-elements
+//original selectors
+//.cssmenu ul, .cssmenu ul li, .cssmenu ul ul
+%extend_1 {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+//original selectors
+//.cssmenu ul li.hover, .cssmenu ul li:hover
+%extend_2 {
+ position: relative;
+ z-index: 599;
+ cursor: default;
+}
+
+
+@charset "UTF-8";
+.cssmenu {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ line-height: 1;
+ width: 100%;
+ background: $panel-bg;
+ font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
+ zoom: 1;
+ font-size: 12px;
+ ul {
+ @extend %extend_1;
+ position: relative;
+ z-index: 597;
+ float: left;
+ li {
+ @extend %extend_1;
+ min-height: 1px;
+ line-height: 3rem;
+ vertical-align: middle;
+ position: relative;
+ float: none;
+ &.hover {
+ @extend %extend_2;
+ }
+ &:hover {
+ @extend %extend_2;
+ > ul {
+ visibility: visible;
+ }
+ }
+ &.has-sub > a:after {
+ content: '+';
+ position: absolute;
+ top: 50%;
+ right: 15px;
+ margin-top: -6px;
+ }
+ }
+ ul {
+ @extend %extend_1;
+ visibility: hidden;
+ position: absolute;
+ z-index: 598;
+ top: 0;
+ left: 100%;
+ margin-top: 0;
+ width: 100%;
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border: 1px solid $panel-bg;
+ li {
+ float: none;
+ font-weight: normal;
+ border-bottom: 1px solid $panel-bg;
+ &.first {
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: 0 3px 0 0;
+ }
+ &.last {
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: 0 0 3px 0;
+ border-bottom: 0;
+ }
+ &:hover > a {
+ background: $button-bg-color;
+ color: $ghost;
+ }
+ }
+ ul {
+ top: -2px;
+ right: 0;
+ }
+ a {
+ font-size: 14px;
+ color: $ghost;
+ &:hover {
+ color: $ghost;
+ }
+ }
+ }
+ }
+ &:before {
+ content: '';
+ display: block;
+ }
+ &:after {
+ content: '';
+ display: table;
+ clear: both;
+ }
+ a {
+ display: block;
+ padding: 15px 20px;
+ color: $ghost;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+ li {
+ position: relative;
+ }
+ &.align-right {
+ float: right;
+ li {
+ text-align: right;
+ }
+ ul {
+ ul {
+ visibility: hidden;
+ position: absolute;
+ top: 0;
+ left: -100%;
+ z-index: 598;
+ width: 100%;
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: $global-radius 0 0 $global-radius;
+ li {
+ &.first {
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: $global-radius 0 0 0;
+ }
+ &.last {
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: 0 0 0 $global-radius;
+ }
+ }
+ }
+ li.has-sub > a {
+ &:before {
+ content: '+';
+ position: absolute;
+ top: 50%;
+ left: 15px;
+ margin-top: -6px;
+ }
+ &:after {
+ content: none;
+ }
+ }
+ }
+ > ul > li > a {
+ border-left: 2px solid lighten($panel-bg, 10%);
+ border-right: none;
+ }
+ }
+ > ul {
+ width: 100%;
+ > li {
+ > a {
+ border-right: 2px solid lighten($panel-bg, 10%);
+ color: $ghost;
+ &:hover {
+ color: $ghost;
+ }
+ }
+ &.active a {
+ background: lighten($panel-bg, 10%);
+ }
+ a:hover {
+ background: lighten($panel-bg, 10%);
+ }
+ &:hover a {
+ background: lighten($panel-bg, 10%);
+ }
+ }
+ }
+}
diff --git a/app/styles/hub/main-nav.scss b/app/styles/hub/main-nav.scss
new file mode 100644
index 0000000000..4683e7b2c0
--- /dev/null
+++ b/app/styles/hub/main-nav.scss
@@ -0,0 +1,132 @@
+/*
+ * These top bar properties still need to be sassified.
+ *
+ */
+
+.topnav-wrapper {
+ background-color: $topbar-dark;
+}
+
+.top-bar .top-bar-section ul li:hover:not(.has-form) > a {
+ background: transparent;
+}
+
+.top-bar-section ul li > a {
+ font-weight: 400;
+ font-size: 14px;
+}
+
+.top-bar-section ul li > a.button.secondary:hover,
+.top-bar-section ul li > a.button.secondary:focus {
+ color: #fff;
+ background: transparent;
+}
+
+.top-bar-section ul li > a.button.secondary {
+ background-color: transparent;
+ color: #c4cdda;
+ font-size: 1em;
+ height: 40px;
+ margin-top: -3px;
+}
+.top-bar-section li:not(.has-form) a:not(.button) {
+ background: transparent;
+ line-height: 3.125rem;
+}
+/*
+ *
+ *
+ */
+
+.top-bar-section {
+ ul li {
+ background: none;
+ a.button.tiny {
+ background-color: #86d800;/** temp color **/
+ border-radius: $global-radius;
+ box-shadow: none;
+ &:hover {
+ background-color: #5fa736;/** temp color **/
+ }
+ }
+ }
+ .profile-photo {
+ width: rem-calc(24px);
+ height: rem-calc(24px);
+ margin-right: 5px;
+ border-radius: $global-radius;
+ }
+ .title-area {
+ margin-top: .5rem;
+ a {
+ padding: 1rem;
+ }
+ img {
+ height: 36px;
+ margin-right: 10px
+ }
+ }
+ .nav-user-info {
+ background: none;
+ }
+ /*
+ * "+" menu
+ *
+ */
+ .css-dropdown {
+ &:hover ul {
+ display: block;
+ opacity: 1;
+ visibility: visible;
+ }
+ ul:before {
+ border: 8px solid transparent;
+ border-bottom-color: #3d4c5a;
+ content: "";
+ display: inline-block;
+ left: 90px;
+ position: absolute;
+ top: -16px;
+ }
+ ul {
+ // background: $topbar-dark;
+ background: #3d4c5a;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ display: none;
+ padding: 0;
+ position: absolute;
+ top: 48px;
+ right: 4rem;
+ opacity: 0;
+ padding: .5rem 0;
+ visibility: hidden;
+ width: 220px;
+ z-index: 1;
+ li {
+ cursor: pointer;
+ float: none;
+ padding: .3em;
+ margin: 0;
+ position: relative;
+ a {
+ display: block;
+ padding-top: .23rem;
+ }
+ a:hover {
+ background: #7a8491;
+ }
+ }
+ li:not(.has-form) > a {
+ line-height: 1.7rem;
+ }
+ & i.fa {
+ font-size: 1.2rem;
+ padding-right: 7px;
+ }
+ &.with-button {
+ right: rem-calc(116px);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/styles/hub/react-tagsinput.scss b/app/styles/hub/react-tagsinput.scss
new file mode 100644
index 0000000000..70287eee8f
--- /dev/null
+++ b/app/styles/hub/react-tagsinput.scss
@@ -0,0 +1,59 @@
+//colors
+$color_celeste_approx: #ccc;
+$white: #fff;
+$color_conifer_approx: #a5d24a;
+$color_deco_approx: #cde69c;
+$color_olive_drab_approx: #638421;
+$color_cosmos_approx: #fbd8db;
+$color_tamarillo_approx: #90111a;
+$color_tapa_approx: #777;
+
+.react-tagsinput {
+ border: 1px solid lighten($panel-bg, 5%);
+ background: lighten($panel-bg, 8%);
+ padding: rem-calc(10px);
+ overflow-y: auto;
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: rem-calc(2px);
+ input {
+ color: $charcoal;
+ }
+}
+.react-tagsinput-tag {
+ display: block;
+ border: 1px solid $secondary-5;
+ background: $secondary-6;
+ color: $oil;
+ font-size: 14px;
+ float: left;
+ padding: 15px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ text-decoration: none;
+ //Instead of the line below you could use @includeborder-radius($radius, $vertical-radius)
+ border-radius: $global-radius;
+}
+.react-tagsinput-invalid {
+ background: transparent;
+ color: $primary-5;
+}
+.react-tagsinput-remove {
+ font-weight: 300;
+ color: $secondary-4;
+ text-decoration: none;
+ font-size: 14px;
+ cursor: pointer;
+ &:before {
+ content: "x";
+ }
+}
+.react-tagsinput-input {
+ background: transparent;
+ color: $charcoal;
+ border: 0;
+ font-size: 13px;
+ padding: 5px;
+ margin: 0;
+ width: 80px;
+ outline: none;
+}
\ No newline at end of file
diff --git a/app/styles/hub/repositories.scss b/app/styles/hub/repositories.scss
new file mode 100644
index 0000000000..3323d95ff8
--- /dev/null
+++ b/app/styles/hub/repositories.scss
@@ -0,0 +1,155 @@
+ul.repositories {
+ margin: 2.25rem 0.3rem 0 0.3rem;
+}
+.repo-header {
+ padding: rem-calc(10px);
+}
+.repo-content {
+ padding: rem-calc(10px);
+}
+.repository {
+ background: white;
+ .title {
+ color: $primary-color;
+ }
+ .logo {
+ width: 64px;
+ min-width: 64px;
+ height: 64px;
+ min-height: 64px;
+ padding: 10px;
+ background: $primary-color;
+ border-radius: $global-radius;
+ .d-logo {
+ color: $ghost;
+ font-size: rem-calc(48px);
+ margin: 0 auto;
+ }
+ }
+ .header {
+ padding-top: 0.325rem;
+ h6 small {
+ font-size: small;
+ }
+ h6 span {
+ font-size: small;
+ text-transform: capitalize;
+ color: #3f5167;
+ }
+ }
+ .repo-wrapper {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ }
+ //TODO: These surely should be `scss-ified`
+ .repo-stars {
+ border: 1px solid #c4cdda;
+ border-left: none;
+ padding: 10px;
+ p {
+ font-size: smaller;
+ }
+ }
+ .repo-downloads {
+ border: 1px solid #c4cdda;
+ border-left: none;
+ border-right: none;
+ padding: 10px;
+ p {
+ font-size: smaller;
+ }
+ }
+}
+
+.repo-separator {
+ font-weight: 300;
+ font-size: rem-calc(24px);
+ margin-right: 0px;
+ padding-right: 0px;
+ width: 20px;
+}
+
+.repo-form-margin {
+ margin-left: -15px;
+}
+
+//Need this to make foundation block-grid work with our design and borders
+.repo-border {
+ border:1px solid white;
+ position:relative;
+ z-index:10
+}
+
+.repo-border:before {
+ content:"";
+ display:block;
+ position:absolute;
+ top:2px;
+ left:2px;
+ right:2px;
+ bottom:2px;
+ border:1px solid #c4cdda;
+ border-radius: $global-radius;
+}
+
+.repository-page {
+ .repo-description-dockerfile {
+ .dockerfile {
+ font-family: $font-family-monospace;
+ white-space: pre;
+ overflow-x: auto;
+ }
+ }
+ .repo-details-content {
+ .repo-visibility {
+ margin-bottom: 1.25rem;
+ background: white;
+ border: 1px solid $docker-dark--placeholders;
+ padding: .75rem 1rem;
+ .privacy {
+ font-weight: $font-weight-bold;
+ display: inline-block;
+ }
+ .status {
+ display: inline-block;
+ }
+ }
+
+
+
+ .temp-prof-icon {
+ height: 50px;
+ width: 50px;
+ border-radius: 100%;
+ border: 1px solid black;
+ display: inline-block;
+ margin-right: 10px;
+ }
+ }
+}
+
+.explore-repo-list {
+ margin-top: 1rem;
+}
+
+.add-repository-form {
+ margin-top: rem-calc(20px);
+}
+
+.repo-tags {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ li {
+ float: left;
+ padding: 0.3rem 0.3rem 1rem 0;
+ margin-right: 0.5rem;
+ .repo-tag {
+ background-color: #3c5164;
+ color: white;
+ border-radius: $global-radius;
+ padding: 0.5rem 0.8rem;
+ }
+ }
+}
+
diff --git a/app/styles/hub/repository-settings.scss b/app/styles/hub/repository-settings.scss
new file mode 100644
index 0000000000..bf957d5600
--- /dev/null
+++ b/app/styles/hub/repository-settings.scss
@@ -0,0 +1,49 @@
+.repo-settings {
+ color: $charcoal;
+ padding: 1rem;
+ h1, h2, h3, h4, h5, h6, input, label {
+ color: $charcoal;
+ }
+ .form-panel {
+ @include panel();
+ background: $white;
+ button {
+ margin-right: $default-margin;
+ }
+ }
+ .help-text {
+ padding: 1rem 0 0 .9rem;
+ }
+ /** scope this to repo-settings only **/
+ input[type="text"],
+ .bar {
+ width: 100%;
+ }
+ .repo-visibility {
+ margin-top: 1rem;
+ .visibility-form {
+ .visibility-toggle {
+ cursor: pointer;
+ * {
+ cursor: pointer;
+ }
+ .text-error {
+ color: $primary-5;
+ margin-left: 1rem;
+ }
+ }
+ .disabled {
+ cursor: default;
+ color: $secondary-5;
+ * {
+ cursor: default;
+ }
+ }
+ input {
+ &[name='visibility'] {
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+}
diff --git a/app/styles/hub/reset-password.scss b/app/styles/hub/reset-password.scss
new file mode 100644
index 0000000000..810a6bf95d
--- /dev/null
+++ b/app/styles/hub/reset-password.scss
@@ -0,0 +1,49 @@
+.pass-reset-wrapper {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ width: 100%;
+ &.reset {
+ text-align: center;
+ }
+ .password-reset {
+ background-color: $white;
+ border: 1px solid rgb(233, 237, 240);// TODO: create/utilize variable
+ border-radius: $global-radius;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-height: 270px;
+ h3 {
+ color: $primary-1;
+ font-weight: 300;
+ }
+ &.error h3 {
+ color: $primary-5;
+ }
+ p {
+ color: rgb(113, 125, 144);// TODO: create/utilize variable
+ }
+ form {
+ margin-top: .6rem;
+ }
+ .group {
+ input,
+ .bar {
+ color: rgb(113, 125, 144);// TODO: create/utilize variable
+ width: 100%;
+ }
+ }
+ .resetPassSubmit {
+ background-color:$primary-color;
+ border: none;
+ border-radius: $global-radius;
+ color: $white;
+ float: right;
+ padding: 10px 20px;
+ &:disabled {
+ background-color:rgb(233, 237, 240);// TODO: create/utilize variable
+ }
+ }
+ }
+}
diff --git a/app/styles/hub/search.scss b/app/styles/hub/search.scss
new file mode 100644
index 0000000000..a0a665c2e2
--- /dev/null
+++ b/app/styles/hub/search.scss
@@ -0,0 +1,191 @@
+//search page
+.search-page {
+ width: 100%;
+ overflow-x: hidden;
+ font-weight: 300;
+ .inline-list {
+ margin-top: rem-calc(12px);
+ li {
+ margin-left: 0.7rem;
+ }
+ }
+ select {
+ background-color: transparent;
+ border: 1px solid #c4cdda;
+ transition: border-color 0.15s linear 0s, background 0.15s linear 0s;
+ outline: 0;
+ color: #3f5167;
+ width: rem-calc(100px);
+ font-size: rem-calc(12px);
+ font-weight: 200;
+ border-color: #c4cdda;
+ border-radius: $global-radius;
+ }
+ input[type="text"] {
+ color: #7A8491;
+ }
+
+ .search-results-container {
+ .no-results-item {
+ text-align: center;
+ }
+ }
+
+ //Search Results
+ .search-results-list {
+ ul {
+ list-style-type: none;
+ text-align: center;
+ }
+ ul li {
+ display: inline-block;
+ padding-right: 5px;
+ }
+ .no-results-item {
+ list-style-type: none;
+ font-size: rem-calc(14px);
+ font-weight: normal;
+ }
+ .search-list-item {
+ background-color: white;
+ margin-bottom: rem-calc(10px);//5px 5px 5px 5px;
+ overflow-y: hidden;
+ font-weight: normal;
+ border: 1px solid #c4cdda;
+ border-radius: $global-radius;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ .logo {
+ width: 48px;
+ min-width: 48px;
+ height: 48px;
+ min-height: 48px;
+ padding: 10px;
+ background: $primary-color;
+ border-radius: $global-radius;
+ margin-left: rem-calc(10px);
+ margin-bottom: rem-calc(25px);
+ .d-logo {
+ color: $ghost;
+ font-size: rem-calc(32px);
+ margin: 0 auto;
+ }
+ }
+
+ .search-item-avatar {
+ }
+
+ .search-bar-details {
+ line-height: rem-calc(12px);
+ height: rem-calc(24px);
+ background-color: #c4cdda;
+ p {
+ font-weight: 300;
+ font-size: rem-calc(12px);
+ margin-bottom: 0;
+ line-height: 1.5rem;
+ }
+ }
+
+ .search-item-basic-info {
+ .search-item-name {
+ font-size: 1.4rem;
+ font-weight: 600;
+ }
+ p {
+ font-size: 0.8rem;
+ }
+ .search-item-description {
+ font-size: rem-calc(12px);
+ }
+ }
+
+ .search-item-other-info {
+ font-size: rem-calc(10px);
+ }
+
+ .search-item-badges-stats {
+ ul li {
+ line-height: 0.6rem;
+ border-left: 1px solid #c4cdda;
+ }
+ p {
+ margin-bottom: 0;
+ font-size: rem-calc(12px);
+ font-weight: 500;
+ }
+ .search-item-badges {
+ }
+ .search-item-stats {
+ span {
+ padding-left: rem-calc(4px);
+ }
+ }
+ }
+ }
+ }
+}
+
+.paging-bar {
+ display: inline-flex;
+ float: right;
+ margin-right: rem-calc(25px);
+ .paging-info {
+ align-self: center;
+ }
+ .paging-buttons {
+ align-self: center;
+ button {
+ margin: 0;
+ background-color: $body-bg;
+ color: $docker-dark;
+ padding-right: 0;
+ padding-left: rem-calc(4px);
+ box-shadow: none;
+ &:hover {
+ color: $primary-color;
+ box-shadow: none;
+ }
+ &:focus {
+ color: #7A8491;
+ outline: none;
+ box-shadow: none;
+ }
+ i {
+ font-size: 24;
+ }
+ }
+ button[disabled] {
+ opacity: 0.3;
+ box-shadow: none;
+ }
+ }
+}
+
+.searchbar {
+ input[type=text] {
+ background: #69788a;/** need variable here **/
+ border: 1px solid #4c5968;/** need variable here **/
+ border-radius: $global-radius;
+ color: $white;
+ display: block;
+ height: 2rem;
+ padding: .2rem 2rem;
+ top: .6rem;
+ }
+ i.fa.fa-search {
+ color: #fff;
+ margin-top: -18px;/** lame **/
+ margin-left: 11px;/** lame **/
+ }
+}
+
+// target webkit only meh
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ i.fa.fa-search {
+ margin-top: -16px!important;/** super lame, this can not be permanent **/
+ }
+}
diff --git a/app/styles/hub/tabs.scss b/app/styles/hub/tabs.scss
new file mode 100644
index 0000000000..ef90208ab0
--- /dev/null
+++ b/app/styles/hub/tabs.scss
@@ -0,0 +1,26 @@
+.tabs {
+ position: relative;
+ float: left;
+ width: 100%;
+ li:first-child a {
+ border-top-left-radius: $global-radius;
+ }
+ li:last-child a {
+ border-top-right-radius: $global-radius;
+ border-right: 1px solid $docker-dark--placeholders;
+ }
+ a.repo-tab {
+ border-left: 1px solid $docker-dark--placeholders;
+ border-top: 1px solid $docker-dark--placeholders;
+ border-bottom: 1px solid $docker-dark--placeholders;
+ background:lighten($docker-dark--placeholders, 10%);
+ padding: .5rem 1.8rem;
+ font-size: 16px;
+ &.active {
+ border-bottom: 1px solid white;
+ background: white;
+ position: relative;
+ z-index: 2;
+ }
+ }
+}
diff --git a/app/styles/hub/usericons.scss b/app/styles/hub/usericons.scss
new file mode 100644
index 0000000000..ca476b3191
--- /dev/null
+++ b/app/styles/hub/usericons.scss
@@ -0,0 +1,6 @@
+.profile-photo {
+ width: rem-calc(24px);
+ height: rem-calc(24px);
+ margin-right: 5px;
+ border-radius: $global-radius;
+}
\ No newline at end of file
diff --git a/app/styles/main.scss b/app/styles/main.scss
new file mode 100644
index 0000000000..c7ee2ba6c6
--- /dev/null
+++ b/app/styles/main.scss
@@ -0,0 +1,170 @@
+@import 'docker-ux';
+@import 'mono-blue';
+@import '_docker';
+
+//============================REMOVE THIS======================================
+
+$default-margin: 1.5rem;
+
+.flex-table {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ border: 1px solid $border-input;
+ border-radius: $global-radius;
+ font-size: 1rem;
+ margin: 0.5rem;
+ line-height: 1.5;
+ font-weight: 500;
+ color: $secondary-2;
+ .flex-row {
+ width: 100%;
+ display: flex;
+ border-bottom: 1px solid $secondary-5;
+ background: white;
+ &:last-child {
+ border-bottom: 0;
+ }
+ &.header {
+ background-color: $primary-1;
+ font-weight: 700;
+ color: white;
+ font-size: 1.2rem;
+ border-bottom: 0;
+ }
+ .flex-item {
+ display: flex;
+ flex-flow: row nowrap;
+ flex-grow: 1;
+ flex-basis: 0;
+ padding: 0.7em 1.2rem 0.7em 1.2rem;
+ word-break: break-word;
+ a {
+ color: $primary-1;
+ &:hover {
+ color: darken($primary-1, 10%);
+ }
+ }
+ }
+ }
+}
+
+//=============================================================================
+
+@import 'hub';
+
+.button {
+ // seems to feel unresponsive with almost any transition length
+ transition: 0.1s;
+ font-weight: 500;
+}
+
+//TODO: could be global style
+.create-object-btn {
+ background-color: white;
+ border: 4px dashed #c4cdda;
+ border-radius: $global-radius;
+ box-shadow: none;
+ color: $docker-dark;
+ line-height: 2.65rem;
+ margin: rem-calc(20px);
+ padding: 0.7rem;
+ font-size: large;
+ text-align: center;
+ width: 90%;
+ cursor: pointer;
+ &:hover {
+ background-color: $docker-light;
+ color: $docker-dark;
+ }
+ &:focus {
+ background-color: $docker-light;
+ color: $docker-dark;
+ }
+ i {
+ float: right;
+ }
+}
+
+//TODO: could be global style
+.blank-slate {
+ min-height: 600px;
+}
+
+.page-top-header {
+ background-color: white;
+ h3 {
+ color: #22b8eb;
+ font-weight: 300;
+ }
+}
+
+.temp-page {
+ margin-top: 2rem;
+ text-align: center;
+ h1 {
+ font-size: 24px;
+ font-weight: 300;
+ color: lighten(gray, 15%);
+ }
+}
+
+
+ select {
+ background-color: transparent;
+ border: 1px solid #c4cdda;
+ transition: border-color 0.15s linear 0s, background 0.15s linear 0s;
+ outline: 0;
+ color: #3f5167;
+ width: rem-calc(100px);
+ font-size: rem-calc(14px);
+ font-weight: 300;
+ border-color: #c4cdda;
+ border-radius: $global-radius;
+ }
+
+form .row textarea.columns {
+ padding-left: 1rem;
+}
+
+.global-select {
+ .text {
+ margin-right: 1rem;
+ color: $secondary-4;
+ }
+}
+
+.alert-box {
+ border-radius: $global-radius;
+}
+
+//TODO: fix in dux
+.sk-cube {
+ background-color: #546473 !important;
+}
+
+/* Pre-move to docker-ux */
+@import 'common/_secondary-top-bar';
+@import 'common/_repository-list-item';
+
+// Don't kill me
+.mktoForm {
+ margin-top: 10px;
+ select#Country {
+ margin-top: 20px;
+ }
+ #Phone {
+ margin-top: 20px;
+ }
+ #accept_eval_terms {
+ margin-top: 30px;
+ }
+}
+.mktoForm div.mktoFormRow {
+ padding-bottom: 20px;
+}
+.mktoFieldWrap {
+ label {
+ top: -15px;
+ }
+}
diff --git a/app/styles/vendor-overrides/rc-tooltip.css b/app/styles/vendor-overrides/rc-tooltip.css
new file mode 100644
index 0000000000..ab6f60e661
--- /dev/null
+++ b/app/styles/vendor-overrides/rc-tooltip.css
@@ -0,0 +1,31 @@
+.rc-tooltip-inner {
+ border: 0 none;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+}
+.rc-tooltip-placement-top .rc-tooltip-arrow {
+ border-left: 0 none;
+ border-top: 0 none;
+ background: #fff;
+ width: 10px;
+ height: 10px;
+ transform: rotate(45deg);
+ border-right: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ z-index: 999;
+}
+
+.rc-tooltip-placement-bottom .rc-tooltip-arrow {
+ background: #fff;
+ width: 10px;
+ height: 10px;
+ transform: rotate(45deg);
+ border-right: 0 none;
+ border-bottom: 0 none;
+ border-left: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+ z-index: 999;
+}
+
+.rc-tooltip-inner * {
+ word-wrap: break-word;
+}
diff --git a/app/styles/vendor-overrides/react-select.css b/app/styles/vendor-overrides/react-select.css
new file mode 100644
index 0000000000..dbce7cfccd
--- /dev/null
+++ b/app/styles/vendor-overrides/react-select.css
@@ -0,0 +1,279 @@
+/**
+ * React Select
+ * ============
+ * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
+ * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
+ * MIT License: https://github.com/keystonejs/react-select
+ *
+ * Modified to match our styles
+*/
+
+.Select {
+ position: relative;
+}
+.Select-control {
+ position: relative;
+ overflow: hidden;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border-color: #d9d9d9 #ccc #b3b3b3;
+ border-radius: 3px;
+ box-sizing: border-box;
+ color: #333;
+ cursor: default;
+ outline: none;
+ padding: 8px 52px 8px 10px;
+}
+.Select-control:hover {
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
+}
+.is-searchable.is-open > .Select-control {
+ cursor: text;
+}
+.is-open > .Select-control {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ background: #fff;
+ border-color: #b3b3b3 #ccc #d9d9d9;
+}
+.is-open > .Select-control > .Select-arrow {
+ border-color: transparent transparent #999;
+ border-width: 0 5px 5px;
+}
+.is-searchable.is-focused:not(.is-open) > .Select-control {
+ cursor: text;
+}
+.is-focused:not(.is-open) > .Select-control {
+ border-color: #08c #0099e6 #0099e6;
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
+}
+.Select-placeholder {
+ color: #aaa;
+ padding: 4px 52px 8px 10px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: -15px;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.has-value > .Select-control > .Select-placeholder {
+ color: #333;
+}
+.Select-value {
+ color: #aaa;
+ padding: 4px 52px 8px 10px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: -15px;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.has-value > .Select-control > .Select-value {
+ color: #333;
+}
+.Select-input > input {
+ cursor: default;
+ background: none transparent;
+ box-shadow: none;
+ height: auto;
+ border: 0 none;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ padding: 0;
+ outline: none;
+ display: inline-block;
+ -webkit-appearance: none;
+}
+.is-focused .Select-input > input {
+ cursor: text;
+}
+.Select-control:not(.is-searchable) > .Select-input {
+ outline: none;
+}
+.Select-loading {
+ -webkit-animation: Select-animation-spin 400ms infinite linear;
+ -o-animation: Select-animation-spin 400ms infinite linear;
+ animation: Select-animation-spin 400ms infinite linear;
+ width: 16px;
+ height: 16px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ border: 2px solid #ccc;
+ border-right-color: #333;
+ display: inline-block;
+ position: relative;
+ margin-top: -8px;
+ position: absolute;
+ right: 30px;
+ top: 50%;
+}
+.has-value > .Select-control > .Select-loading {
+ right: 46px;
+}
+.Select-clear {
+ color: #999;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 12px;
+ padding: 6px 10px;
+ position: absolute;
+ right: 17px;
+ top: 0;
+}
+.Select-clear:hover {
+ color: #c0392b;
+}
+.Select-clear > span {
+ font-size: 0.8rem;
+}
+.Select-arrow-zone {
+ content: " ";
+ display: block;
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 30px;
+ cursor: pointer;
+}
+.Select-arrow {
+ border-color: #999 transparent transparent;
+ border-style: solid;
+ border-width: 5px 5px 0;
+ content: " ";
+ display: block;
+ height: 0;
+ margin-top: -ceil(2.5px);
+ position: absolute;
+ right: 10px;
+ top: 14px;
+ width: 0;
+ cursor: pointer;
+}
+.Select-menu-outer {
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border-top-color: #e6e6e6;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
+ box-sizing: border-box;
+ margin-top: -1px;
+ max-height: 200px;
+ position: absolute;
+ top: 100%;
+ width: 100%;
+ z-index: 1000;
+ -webkit-overflow-scrolling: touch;
+}
+.Select-menu {
+ max-height: 198px;
+ overflow-y: auto;
+}
+.Select-option {
+ box-sizing: border-box;
+ color: #666666;
+ cursor: pointer;
+ display: block;
+ padding: 8px 10px;
+}
+.Select-option:last-child {
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px;
+}
+.Select-option.is-focused {
+ background-color: #f2f9fc;
+ color: #333;
+}
+.Select-option.is-disabled {
+ color: #cccccc;
+ cursor: not-allowed;
+}
+.Select-noresults,
+.Select-search-prompt,
+.Select-searching {
+ box-sizing: border-box;
+ color: #999999;
+ cursor: default;
+ display: block;
+ padding: 8px 10px;
+}
+.Select.is-multi .Select-control {
+ padding: 2px 52px 2px 3px;
+}
+.Select.is-multi .Select-input {
+ vertical-align: middle;
+ border: 1px solid transparent;
+ margin: 2px;
+ padding: 3px 0;
+}
+.Select-item {
+ background-color: #f2f9fc;
+ border-radius: 2px;
+ border: 1px solid #c9e6f2;
+ color: #08c;
+ display: inline-block;
+ font-size: 0.8rem;
+ margin: 2px;
+}
+.Select-item-icon,
+.Select-item-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.Select-item-label {
+ cursor: default;
+ border-bottom-right-radius: 2px;
+ border-top-right-radius: 2px;
+ padding: 3px 5px;
+}
+.Select-item-label .Select-item-label__a {
+ color: #08c;
+ cursor: pointer;
+}
+.Select-item-icon {
+ cursor: pointer;
+ border-bottom-left-radius: 2px;
+ border-top-left-radius: 2px;
+ border-right: 1px solid #c9e6f2;
+ padding: 2px 5px 4px;
+}
+.Select-item-icon:hover,
+.Select-item-icon:focus {
+ background-color: #ddeff7;
+ color: #0077b3;
+}
+.Select-item-icon:active {
+ background-color: #c9e6f2;
+}
+.Select.is-multi.is-disabled .Select-item {
+ background-color: #f2f2f2;
+ border: 1px solid #d9d9d9;
+ color: #888;
+}
+.Select.is-multi.is-disabled .Select-item-icon {
+ cursor: not-allowed;
+ border-right: 1px solid #d9d9d9;
+}
+.Select.is-multi.is-disabled .Select-item-icon:hover,
+.Select.is-multi.is-disabled .Select-item-icon:focus,
+.Select.is-multi.is-disabled .Select-item-icon:active {
+ background-color: #f2f2f2;
+}
+@keyframes Select-animation-spin {
+ to {
+ transform: rotate(1turn);
+ }
+}
+@-webkit-keyframes Select-animation-spin {
+ to {
+ -webkit-transform: rotate(1turn);
+ }
+}
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000000..fee94207b8
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,41 @@
+machine:
+ pre:
+ - echo 'DOCKER_OPTS="-s btrfs -e lxc -D --userland-proxy=false"' | sudo tee -a /etc/default/docker
+ - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.8.2-circleci-cp-workaround'
+ - sudo chmod 0755 /usr/bin/docker
+ node:
+ version: 4.1.0
+ services:
+ - docker
+dependencies:
+ override:
+ - mkdir -p ./bin
+ - pip install fabric==1.8.1
+ - pip install pycrypto
+ - make hub-deps
+test:
+ override:
+ - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_AUTH
+ - docker build -f dockerfiles/milky-way-no-bin -t milky-no-bin ./dockerfiles
+ - make stage
+ - mkdir .stage/
+ - make copy-stage
+ - docker run -d --name milky milky-no-bin sleep 15m
+ - cd .stage/.build-prod && docker cp milky:/opt/hub/modules.tar . && docker build -t bagel/hub-stage .
+ - make prod
+ - make base-prod-tag
+ - make copy-prod
+ - docker run -d --name milky-2 milky-no-bin sleep 15m
+ - cd .build-prod && docker cp milky-2:/opt/hub/modules.tar . && docker build -t bagel/hub-prod .
+ - docker run -de ENV=production -p 3000:3000 --name hub-prod-tester bagel/hub-prod
+ - sleep 60s
+ - curl $(docker inspect --format '{{ .NetworkSettings.IPAddress }}' hub-prod-tester):3000
+deployment:
+ autodeploy:
+ branch: [master, autodeploy]
+ owner: docker
+ commands:
+ - docker push bagel/hub-prod
+ - docker push bagel/hub-stage
+ - chmod 400 ~/.ssh/id_console-demo
+ - fab -H root@console-demo.docker.com -i ~/.ssh/id_console-demo start_project:email=$DOCKER_EMAIL,user=$DOCKER_USER,auth=$DOCKER_AUTH,beta_password=$BETA_PASSWORD,sha=latest,new_relic_key=$NEW_RELIC_KEY,new_relic_app_name=$NEW_RELIC_APP_NAME
diff --git a/containers/README.md b/containers/README.md
new file mode 100644
index 0000000000..6cb24e12b8
--- /dev/null
+++ b/containers/README.md
@@ -0,0 +1,48 @@
+
+# Containers
+
+These are the "accessory" containers with which Hub 2.0 is run.
+
+## dnsmasq
+
+dnsmasq is used to fake the `Origin` header in CORS requests. This is
+necessary because the browser automatically sends `Origin: localhost`
+(users can't modify it) and we need it to be in the `*.docker.com`
+space, since staging is set up to handle single dot subdomains.
+
+We've chosen `bagels.docker.com` as the development domain (something
+that is unlikely to ever be deployed in production so that we won't
+have to change the name in the future).
+
+### prerequisites
+
+```bash
+cd $PROJECT
+make dns
+```
+
+This runs `$PROJECT/containers/configure_system_dns.sh`, which will
+add `bagels.docker.com` to your host system's `/etc/resolver/`. This
+makes it so that `bagels.docker.com` will resolver to `boot2docker ip`.
+
+### run
+
+```bash
+cd $PROJECT/containers/dnsmasq
+docker build -t bagelteam/dnsmasq
+docker run -itp 53:53/udp bagelteam/dnsmasq
+```
+
+## HAProxy
+
+HAProxy is a load balancer used to terminate SSL.
+
+Currently Out-of-Order.
+
+```bash
+docker run -itp 80:80 -p 443:433 bagelteam/haproxy
+```
+
+HAProxy will load balance `bagels.docker.com` across a single
+container (hah), and more importantly, take care of SSL Offloading at
+the load balancer. The image has it's own SSL certificates.
diff --git a/containers/dnsmasq/Dockerfile b/containers/dnsmasq/Dockerfile
new file mode 100644
index 0000000000..40c00e83d5
--- /dev/null
+++ b/containers/dnsmasq/Dockerfile
@@ -0,0 +1,12 @@
+FROM debian:jessie
+
+MAINTAINER Chris Biscardi
+
+RUN apt-get update && apt-get install -y dnsmasq
+
+EXPOSE 53/udp
+
+ADD ./run /opt/run
+
+CMD "/opt/run"
+# docker run -d -p 53:53/udp --name docker-dnsmasq dnsmasq --address=/dev.docker.io/172.16.200.100
diff --git a/containers/dnsmasq/configure_system_dns.sh b/containers/dnsmasq/configure_system_dns.sh
new file mode 100755
index 0000000000..d84112198a
--- /dev/null
+++ b/containers/dnsmasq/configure_system_dns.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# Docker Bridge IP - https://docs.docker.com/articles/networking/
+# $DOCKER_HOST will be the IP of the boot2docker or docker-machine
+# instance *currently sourced in your shell*. This means something
+# like $(docker-machine env dev) or $(boot2docker shellinit)
+if [[ $DOCKER_HOST =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]]; then
+ DAEMON_IPV4=$BASH_REMATCH
+ echo $DAEMON_IPV4
+else
+ echo "unable to parse string $DOCKER_HOST"
+fi
+
+set_dev_resolver() {
+ echo "Bagels need your permission to configure system DNS."
+ sudo mkdir -p /etc/resolver
+ echo "nameserver $DAEMON_IPV4" | sudo tee /etc/resolver/bagels.docker.com
+}
+
+if [ ! -f /etc/resolver/bagels.docker.com ]; then
+ set_dev_resolver
+elif [ "$(cat /etc/resolver/bagels.docker.com)" != "nameserver $DAEMON_IPV4" ]; then
+ set_dev_resolver
+fi
diff --git a/containers/dnsmasq/run b/containers/dnsmasq/run
new file mode 100755
index 0000000000..b2b60ad240
--- /dev/null
+++ b/containers/dnsmasq/run
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# match the ip address from a DOCKER_HOST which is set by boot2docker
+# and docker-machine
+if [[ $DOCKER_HOST =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]]; then
+ strresult=$BASH_REMATCH
+ echo $strresult
+else
+ echo "unable to parse string $DOCKER_HOST"
+fi
+
+/usr/sbin/dnsmasq -q --no-daemon --address=/bagels.docker.com/$strresult
+#$strresult
diff --git a/containers/haproxy/Dockerfile b/containers/haproxy/Dockerfile
new file mode 100644
index 0000000000..034260c7e2
--- /dev/null
+++ b/containers/haproxy/Dockerfile
@@ -0,0 +1,10 @@
+FROM fish/haproxy
+
+ADD . /haproxy
+
+EXPOSE 80 443
+
+# Check is haproxy.cfg is valid before we start
+# CMD "(haproxy -c -f /haproxy/haproxy.cfg || ( echo 'Bad haproxy config'; exit; )) && /usr/sbin/haproxy -f /haproxy/haproxy.cfg & && wait $!"
+
+ENTRYPOINT ["/haproxy/run"]
\ No newline at end of file
diff --git a/containers/haproxy/haproxy.cfg b/containers/haproxy/haproxy.cfg
new file mode 100644
index 0000000000..b9d1c5014a
--- /dev/null
+++ b/containers/haproxy/haproxy.cfg
@@ -0,0 +1,41 @@
+global
+ chroot /var/lib/haproxy
+ user haproxy
+ group haproxy
+
+defaults
+ log global
+ mode http
+ option httplog
+ option dontlognull
+ timeout connect 5000
+ timeout client 50000
+ timeout server 50000
+ errorfile 400 /etc/haproxy/errors/400.http
+ errorfile 403 /etc/haproxy/errors/403.http
+ errorfile 408 /etc/haproxy/errors/408.http
+ errorfile 500 /etc/haproxy/errors/500.http
+ errorfile 502 /etc/haproxy/errors/502.http
+ errorfile 503 /etc/haproxy/errors/503.http
+ errorfile 504 /etc/haproxy/errors/504.http
+ stats enable
+ stats auth haproxy:hapass
+
+frontend https
+ bind :443 ssl crt /haproxy/keys/bagels.docker.com/bagels.docker.pem
+ acl is-ssl dst_port 443
+
+ http-request set-header X-Real-IP %ci
+
+ reqadd X-Forwarded-Proto:\ https if is-ssl
+ reqadd X-Forwarded-Port:\ 443 if is-ssl
+ rspadd Strict-Transport-Security:\ max-age=31536000 if is-ssl
+
+ acl is_hub_dev hdr(host) -i bagels.docker.com
+
+ use_backend hub_dev if is_hub_dev
+
+backend hub_dev
+ balance leastconn
+ option httpclose
+ server docker-1 {DOCKER_HOST}:7001 check
\ No newline at end of file
diff --git a/containers/haproxy/keys/bagels.docker.com/bagels.docker.crt b/containers/haproxy/keys/bagels.docker.com/bagels.docker.crt
new file mode 100644
index 0000000000..44765408fb
--- /dev/null
+++ b/containers/haproxy/keys/bagels.docker.com/bagels.docker.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICNTCCAZ4CCQDY33gN8y9BQzANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoT
+BkRvY2tlcjEaMBgGA1UEAxMRYmFnZWxzLmRvY2tlci5jb20wHhcNMTUwMTIxMDM1
+NTEzWhcNMTYwMTIxMDM1NTEzWjBfMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjEaMBgGA1UE
+AxMRYmFnZWxzLmRvY2tlci5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
+AMluBCvOUrdFkFGCmpKBPduoZgYE/hNKnmX3Cqrn+FsodOBiin1lOs7+XX3EY078
+u5QIULNZ3j/LUSuxgHBS8RcVc3ljCkvwRURwVy6FWunahdTULLEq+qOByv6Hq2/W
+itlzT2Rw6Tu29IThb7Mtxb1B6LoAorkWX/YEXankpVPnAgMBAAEwDQYJKoZIhvcN
+AQEFBQADgYEAPdqZ2jLxOuZ52wucJN1DoOBUCWnCM5bfBHOU3wBqSPA/mT2Bw5Fo
+evqqd+mRWizgmSkDM9NpO9cj9tpeidTrHsTutkqjQttIeNAtZm82sSWH7ul1N1du
+4aDDKwAk4j9BYPUmYQFaSRKNtE/OpGVPxxK/ZBS8YeVT0knzTr/a9to=
+-----END CERTIFICATE-----
diff --git a/containers/haproxy/keys/bagels.docker.com/bagels.docker.csr b/containers/haproxy/keys/bagels.docker.com/bagels.docker.csr
new file mode 100644
index 0000000000..b9d96abb7e
--- /dev/null
+++ b/containers/haproxy/keys/bagels.docker.com/bagels.docker.csr
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBnzCCAQgCAQAwXzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKEwZEb2NrZXIxGjAYBgNVBAMTEWJhZ2Vs
+cy5kb2NrZXIuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJbgQrzlK3
+RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9ZTrO/l19xGNO/LuUCFCzWd4/
+y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1CyxKvqjgcr+h6tv1orZc09kcOk7
+tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQABoAAwDQYJKoZIhvcNAQEFBQAD
+gYEAlAQKhy4j7wenWqnKzfpp/o0cbzQAcve76XSwfWrzONFDZidhQlwAKBdYbYN3
+4ITqNw4MPSCMBkMMCQFFFHM/+NqlAmYYbJHv8uDxKel/7IsxIEPRun0b6k/+wL2e
+2nyJJrMwesVrzvDwfB+8eoUOZFJIiX6htpxU4vgq9xMgMAg=
+-----END CERTIFICATE REQUEST-----
diff --git a/containers/haproxy/keys/bagels.docker.com/bagels.docker.key b/containers/haproxy/keys/bagels.docker.com/bagels.docker.key
new file mode 100644
index 0000000000..6659dc144c
--- /dev/null
+++ b/containers/haproxy/keys/bagels.docker.com/bagels.docker.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQDJbgQrzlK3RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9
+ZTrO/l19xGNO/LuUCFCzWd4/y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1Cyx
+Kvqjgcr+h6tv1orZc09kcOk7tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQAB
+AoGAcA+Qqn5Cbkt5Gp+6Nr9IFqf8+mRUpY7hyIBDowkiljRPsXWg7loe+YFxqcJU
+LWFVSenGW8Enb/5AzjoV5md+UAiERbF13SzrEx7J7riwb1ljHe82RqUbyfpnnDWT
+aSZ/ce+9LYoYggFVfH7DloT8NNsQzTDi2/g+66dXi8fcIIECQQDqykIcF20sz8Ar
+H3StlgITEorJiZRpvbzuQ6G7XoC1XOI1/0+1NbIHm3lv4XfvaRSdKv0mnPaZmi3h
+PWZC/zj3AkEA26BE6iEGO3eJ39l1zRpB1jS/VrEAa7pzGeEFeL+k/5XEWqsgHOtQ
+qIbPtCyKcN5mtCYEg6GEK/pqNALWL6ZwkQJATT9CRO/IMaggd4+f2cSy5geBthEX
+zTppwJJr0bOj8QegPVfEp8AE1M/oQlESHqiZ6aPNKjkWQS8izSpgTMafvQJAILDT
+cTInNlTNvfcldLkS0aqaTHIeSOrA1TpMUTPdgHmvd3t/VS6lm+AtLHlwxeokyW3b
+QCibftxQUJuXfBI/MQJAAt2m8P0V+U/MFjNhYUd2jwJIFFh7AVYeSH26NxzQMgO0
+YNQAaRKxwuhDrxyVwezryzyBcVWKdfhCtgOK6U5mFw==
+-----END RSA PRIVATE KEY-----
diff --git a/containers/haproxy/keys/bagels.docker.com/bagels.docker.pem b/containers/haproxy/keys/bagels.docker.com/bagels.docker.pem
new file mode 100644
index 0000000000..753429af60
--- /dev/null
+++ b/containers/haproxy/keys/bagels.docker.com/bagels.docker.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIICNTCCAZ4CCQDY33gN8y9BQzANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoT
+BkRvY2tlcjEaMBgGA1UEAxMRYmFnZWxzLmRvY2tlci5jb20wHhcNMTUwMTIxMDM1
+NTEzWhcNMTYwMTIxMDM1NTEzWjBfMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjEaMBgGA1UE
+AxMRYmFnZWxzLmRvY2tlci5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
+AMluBCvOUrdFkFGCmpKBPduoZgYE/hNKnmX3Cqrn+FsodOBiin1lOs7+XX3EY078
+u5QIULNZ3j/LUSuxgHBS8RcVc3ljCkvwRURwVy6FWunahdTULLEq+qOByv6Hq2/W
+itlzT2Rw6Tu29IThb7Mtxb1B6LoAorkWX/YEXankpVPnAgMBAAEwDQYJKoZIhvcN
+AQEFBQADgYEAPdqZ2jLxOuZ52wucJN1DoOBUCWnCM5bfBHOU3wBqSPA/mT2Bw5Fo
+evqqd+mRWizgmSkDM9NpO9cj9tpeidTrHsTutkqjQttIeNAtZm82sSWH7ul1N1du
+4aDDKwAk4j9BYPUmYQFaSRKNtE/OpGVPxxK/ZBS8YeVT0knzTr/a9to=
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQDJbgQrzlK3RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9
+ZTrO/l19xGNO/LuUCFCzWd4/y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1Cyx
+Kvqjgcr+h6tv1orZc09kcOk7tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQAB
+AoGAcA+Qqn5Cbkt5Gp+6Nr9IFqf8+mRUpY7hyIBDowkiljRPsXWg7loe+YFxqcJU
+LWFVSenGW8Enb/5AzjoV5md+UAiERbF13SzrEx7J7riwb1ljHe82RqUbyfpnnDWT
+aSZ/ce+9LYoYggFVfH7DloT8NNsQzTDi2/g+66dXi8fcIIECQQDqykIcF20sz8Ar
+H3StlgITEorJiZRpvbzuQ6G7XoC1XOI1/0+1NbIHm3lv4XfvaRSdKv0mnPaZmi3h
+PWZC/zj3AkEA26BE6iEGO3eJ39l1zRpB1jS/VrEAa7pzGeEFeL+k/5XEWqsgHOtQ
+qIbPtCyKcN5mtCYEg6GEK/pqNALWL6ZwkQJATT9CRO/IMaggd4+f2cSy5geBthEX
+zTppwJJr0bOj8QegPVfEp8AE1M/oQlESHqiZ6aPNKjkWQS8izSpgTMafvQJAILDT
+cTInNlTNvfcldLkS0aqaTHIeSOrA1TpMUTPdgHmvd3t/VS6lm+AtLHlwxeokyW3b
+QCibftxQUJuXfBI/MQJAAt2m8P0V+U/MFjNhYUd2jwJIFFh7AVYeSH26NxzQMgO0
+YNQAaRKxwuhDrxyVwezryzyBcVWKdfhCtgOK6U5mFw==
+-----END RSA PRIVATE KEY-----
diff --git a/containers/haproxy/run b/containers/haproxy/run
new file mode 100755
index 0000000000..b26edf0b91
--- /dev/null
+++ b/containers/haproxy/run
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# match the ip address from a DOCKER_HOST which is set by boot2docker
+# and docker-machine
+if [[ $DOCKER_HOST =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]]; then
+ strresult=$BASH_REMATCH
+ echo $strresult
+else
+ echo "unable to parse string $DOCKER_HOST"
+fi
+
+sed -i s/{DOCKER_HOST}/"$strresult"/g /haproxy/haproxy.cfg
+
+# Check is haproxy.cfg is valid before we start
+haproxy -c -f /haproxy/haproxy.cfg || ( echo 'Bad haproxy config'; exit; )
+
+/usr/sbin/haproxy -f /haproxy/haproxy.cfg &
+
+wait $!
diff --git a/containers/prod/build b/containers/prod/build
new file mode 100755
index 0000000000..af29f19d61
--- /dev/null
+++ b/containers/prod/build
@@ -0,0 +1 @@
+docker build -t bagel/haproxy_beta ./haproxy
\ No newline at end of file
diff --git a/containers/prod/docker-compose.yml b/containers/prod/docker-compose.yml
new file mode 100644
index 0000000000..838d385d12
--- /dev/null
+++ b/containers/prod/docker-compose.yml
@@ -0,0 +1,13 @@
+haproxy:
+ build: ./haproxy
+ ports:
+ - "80:80"
+ - "443:443"
+hub:
+ build: bagelteam/hubtest
+ volumes:
+ - .:/opt/hub
+ ports:
+ - "7001:3000"
+ environment:
+ ENV: production
\ No newline at end of file
diff --git a/containers/prod/haproxy/Dockerfile b/containers/prod/haproxy/Dockerfile
new file mode 100644
index 0000000000..12a913f9de
--- /dev/null
+++ b/containers/prod/haproxy/Dockerfile
@@ -0,0 +1,9 @@
+FROM fish/haproxy
+
+ADD ./haproxy.cfg /haproxy/haproxy.cfg
+ADD ./run /haproxy/run
+
+EXPOSE 80 443
+
+ENTRYPOINT ["/haproxy/run"]
+
diff --git a/containers/prod/haproxy/haproxy.cfg b/containers/prod/haproxy/haproxy.cfg
new file mode 100644
index 0000000000..72a144b20e
--- /dev/null
+++ b/containers/prod/haproxy/haproxy.cfg
@@ -0,0 +1,46 @@
+global
+ chroot /var/lib/haproxy
+ user haproxy
+ group haproxy
+
+defaults
+ log global
+ mode http
+ option httplog
+ option dontlognull
+ timeout connect 5000
+ timeout client 50000
+ timeout server 50000
+ errorfile 400 /etc/haproxy/errors/400.http
+ errorfile 403 /etc/haproxy/errors/403.http
+ errorfile 408 /etc/haproxy/errors/408.http
+ errorfile 500 /etc/haproxy/errors/500.http
+ errorfile 502 /etc/haproxy/errors/502.http
+ errorfile 503 /etc/haproxy/errors/503.http
+ errorfile 504 /etc/haproxy/errors/504.http
+ stats enable
+ stats auth haproxy:hapass
+
+userlist Bagels
+ user betalist insecure-password {BETA_PASSWORD}
+
+frontend https
+ bind :443 ssl crt /haproxy/keys/hub-beta.docker.com/hub-beta.docker.pem
+ acl is-ssl dst_port 443
+ acl Auth_Bagels http_auth(Bagels)
+ http-request auth realm HubBeta if !Auth_Bagels
+
+ http-request set-header X-Real-IP %ci
+
+ reqadd X-Forwarded-Proto:\ https if is-ssl
+ reqadd X-Forwarded-Port:\ 443 if is-ssl
+ rspadd Strict-Transport-Security:\ max-age=31536000 if is-ssl
+
+ acl is_hub_dev hdr(host) -i hub-beta.docker.com
+
+ use_backend hub_dev if is_hub_dev
+
+backend hub_dev
+ balance leastconn
+ option httpclose
+ server docker-1 172.17.42.1:7001 check
diff --git a/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.crt b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.crt
new file mode 100644
index 0000000000..44765408fb
--- /dev/null
+++ b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICNTCCAZ4CCQDY33gN8y9BQzANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoT
+BkRvY2tlcjEaMBgGA1UEAxMRYmFnZWxzLmRvY2tlci5jb20wHhcNMTUwMTIxMDM1
+NTEzWhcNMTYwMTIxMDM1NTEzWjBfMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjEaMBgGA1UE
+AxMRYmFnZWxzLmRvY2tlci5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
+AMluBCvOUrdFkFGCmpKBPduoZgYE/hNKnmX3Cqrn+FsodOBiin1lOs7+XX3EY078
+u5QIULNZ3j/LUSuxgHBS8RcVc3ljCkvwRURwVy6FWunahdTULLEq+qOByv6Hq2/W
+itlzT2Rw6Tu29IThb7Mtxb1B6LoAorkWX/YEXankpVPnAgMBAAEwDQYJKoZIhvcN
+AQEFBQADgYEAPdqZ2jLxOuZ52wucJN1DoOBUCWnCM5bfBHOU3wBqSPA/mT2Bw5Fo
+evqqd+mRWizgmSkDM9NpO9cj9tpeidTrHsTutkqjQttIeNAtZm82sSWH7ul1N1du
+4aDDKwAk4j9BYPUmYQFaSRKNtE/OpGVPxxK/ZBS8YeVT0knzTr/a9to=
+-----END CERTIFICATE-----
diff --git a/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.csr b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.csr
new file mode 100644
index 0000000000..b9d96abb7e
--- /dev/null
+++ b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.csr
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBnzCCAQgCAQAwXzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKEwZEb2NrZXIxGjAYBgNVBAMTEWJhZ2Vs
+cy5kb2NrZXIuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJbgQrzlK3
+RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9ZTrO/l19xGNO/LuUCFCzWd4/
+y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1CyxKvqjgcr+h6tv1orZc09kcOk7
+tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQABoAAwDQYJKoZIhvcNAQEFBQAD
+gYEAlAQKhy4j7wenWqnKzfpp/o0cbzQAcve76XSwfWrzONFDZidhQlwAKBdYbYN3
+4ITqNw4MPSCMBkMMCQFFFHM/+NqlAmYYbJHv8uDxKel/7IsxIEPRun0b6k/+wL2e
+2nyJJrMwesVrzvDwfB+8eoUOZFJIiX6htpxU4vgq9xMgMAg=
+-----END CERTIFICATE REQUEST-----
diff --git a/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.key b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.key
new file mode 100644
index 0000000000..6659dc144c
--- /dev/null
+++ b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQDJbgQrzlK3RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9
+ZTrO/l19xGNO/LuUCFCzWd4/y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1Cyx
+Kvqjgcr+h6tv1orZc09kcOk7tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQAB
+AoGAcA+Qqn5Cbkt5Gp+6Nr9IFqf8+mRUpY7hyIBDowkiljRPsXWg7loe+YFxqcJU
+LWFVSenGW8Enb/5AzjoV5md+UAiERbF13SzrEx7J7riwb1ljHe82RqUbyfpnnDWT
+aSZ/ce+9LYoYggFVfH7DloT8NNsQzTDi2/g+66dXi8fcIIECQQDqykIcF20sz8Ar
+H3StlgITEorJiZRpvbzuQ6G7XoC1XOI1/0+1NbIHm3lv4XfvaRSdKv0mnPaZmi3h
+PWZC/zj3AkEA26BE6iEGO3eJ39l1zRpB1jS/VrEAa7pzGeEFeL+k/5XEWqsgHOtQ
+qIbPtCyKcN5mtCYEg6GEK/pqNALWL6ZwkQJATT9CRO/IMaggd4+f2cSy5geBthEX
+zTppwJJr0bOj8QegPVfEp8AE1M/oQlESHqiZ6aPNKjkWQS8izSpgTMafvQJAILDT
+cTInNlTNvfcldLkS0aqaTHIeSOrA1TpMUTPdgHmvd3t/VS6lm+AtLHlwxeokyW3b
+QCibftxQUJuXfBI/MQJAAt2m8P0V+U/MFjNhYUd2jwJIFFh7AVYeSH26NxzQMgO0
+YNQAaRKxwuhDrxyVwezryzyBcVWKdfhCtgOK6U5mFw==
+-----END RSA PRIVATE KEY-----
diff --git a/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.pem b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.pem
new file mode 100644
index 0000000000..753429af60
--- /dev/null
+++ b/containers/prod/haproxy/keys/bagels.docker.com/bagels.docker.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIICNTCCAZ4CCQDY33gN8y9BQzANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoT
+BkRvY2tlcjEaMBgGA1UEAxMRYmFnZWxzLmRvY2tlci5jb20wHhcNMTUwMTIxMDM1
+NTEzWhcNMTYwMTIxMDM1NTEzWjBfMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkRvY2tlcjEaMBgGA1UE
+AxMRYmFnZWxzLmRvY2tlci5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
+AMluBCvOUrdFkFGCmpKBPduoZgYE/hNKnmX3Cqrn+FsodOBiin1lOs7+XX3EY078
+u5QIULNZ3j/LUSuxgHBS8RcVc3ljCkvwRURwVy6FWunahdTULLEq+qOByv6Hq2/W
+itlzT2Rw6Tu29IThb7Mtxb1B6LoAorkWX/YEXankpVPnAgMBAAEwDQYJKoZIhvcN
+AQEFBQADgYEAPdqZ2jLxOuZ52wucJN1DoOBUCWnCM5bfBHOU3wBqSPA/mT2Bw5Fo
+evqqd+mRWizgmSkDM9NpO9cj9tpeidTrHsTutkqjQttIeNAtZm82sSWH7ul1N1du
+4aDDKwAk4j9BYPUmYQFaSRKNtE/OpGVPxxK/ZBS8YeVT0knzTr/a9to=
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQDJbgQrzlK3RZBRgpqSgT3bqGYGBP4TSp5l9wqq5/hbKHTgYop9
+ZTrO/l19xGNO/LuUCFCzWd4/y1ErsYBwUvEXFXN5YwpL8EVEcFcuhVrp2oXU1Cyx
+Kvqjgcr+h6tv1orZc09kcOk7tvSE4W+zLcW9Qei6AKK5Fl/2BF2p5KVT5wIDAQAB
+AoGAcA+Qqn5Cbkt5Gp+6Nr9IFqf8+mRUpY7hyIBDowkiljRPsXWg7loe+YFxqcJU
+LWFVSenGW8Enb/5AzjoV5md+UAiERbF13SzrEx7J7riwb1ljHe82RqUbyfpnnDWT
+aSZ/ce+9LYoYggFVfH7DloT8NNsQzTDi2/g+66dXi8fcIIECQQDqykIcF20sz8Ar
+H3StlgITEorJiZRpvbzuQ6G7XoC1XOI1/0+1NbIHm3lv4XfvaRSdKv0mnPaZmi3h
+PWZC/zj3AkEA26BE6iEGO3eJ39l1zRpB1jS/VrEAa7pzGeEFeL+k/5XEWqsgHOtQ
+qIbPtCyKcN5mtCYEg6GEK/pqNALWL6ZwkQJATT9CRO/IMaggd4+f2cSy5geBthEX
+zTppwJJr0bOj8QegPVfEp8AE1M/oQlESHqiZ6aPNKjkWQS8izSpgTMafvQJAILDT
+cTInNlTNvfcldLkS0aqaTHIeSOrA1TpMUTPdgHmvd3t/VS6lm+AtLHlwxeokyW3b
+QCibftxQUJuXfBI/MQJAAt2m8P0V+U/MFjNhYUd2jwJIFFh7AVYeSH26NxzQMgO0
+YNQAaRKxwuhDrxyVwezryzyBcVWKdfhCtgOK6U5mFw==
+-----END RSA PRIVATE KEY-----
diff --git a/containers/prod/haproxy/run b/containers/prod/haproxy/run
new file mode 100755
index 0000000000..145832a4d5
--- /dev/null
+++ b/containers/prod/haproxy/run
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+sed -i s/{BETA_PASSWORD}/"$BETA_PASSWORD"/g /haproxy/haproxy.cfg
+
+# Check is haproxy.cfg is valid before we start
+haproxy -c -f /haproxy/haproxy.cfg || ( echo 'Bad haproxy config'; exit; )
+
+/usr/sbin/haproxy -f /haproxy/haproxy.cfg &
+
+wait $!
diff --git a/deployment/deploy.sh b/deployment/deploy.sh
new file mode 100755
index 0000000000..9eded10710
--- /dev/null
+++ b/deployment/deploy.sh
@@ -0,0 +1,171 @@
+#!/bin/sh
+
+DOCKER_CMD=docker
+
+alias AWS_HUB_PROD='aws ec2 describe-instances --filters "Name=tag:aws:cloudformation:stack-name,Values=us-east-1*" "Name=tag:secondary-role,Values=hub" "Name=instance-state-name,Values=running" --output=json'
+alias AWS_HUB_STAGE='aws ec2 describe-instances --filters "Name=tag:aws:cloudformation:stack-name,Values=stage-us-east-1*" "Name=tag:secondary-role,Values=hub" "Name=instance-state-name,Values=running" --output=json'
+alias AWS_IP="jq -r '.Reservations[].Instances[].PrivateIpAddress'"
+
+HUB_GATEWAY="https://hub.docker.com"
+HUB_SERVICE_NAME="hub-web-v2"
+
+DEFAULT_IMAGE_PROD="bagel/hub-prod"
+DEFAULT_IMAGE_STAGE="bagel/hub-stage"
+
+NEW_RELIC_APP_NAME="hub.docker.com(aws-node)"
+NEW_RELIC_LICENSE_KEY="582e3891446a63a3f99b4d32f9585ec74af1d8d7"
+
+NO_COLOR="\033[0m"
+RED="\033[0;31m"
+GREEN="\033[0;32m"
+YELLOW="\033[0;33m"
+
+MESSAGE_MISSING_OR_INVALID_ARGS="${RED}Missing or invalid arguments${NO_COLOR}"
+
+# $1: prod or stage
+getAWSHosts() {
+ if [ $1 == "prod" ]; then
+ echo $(AWS_HUB_PROD | ( AWS_IP ; echo ) | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/:2376 /g')
+ elif [ $1 == "stage" ]; then
+ echo $(AWS_HUB_STAGE | ( AWS_IP ; echo ) | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/:2376 /g')
+ fi
+}
+
+# $1: Exit code
+printUsageAndExit() {
+ echo
+ echo "Usage: deploy.sh [prod|stage|-h ] [IMAGE]"
+ echo
+ echo " prod A predefined list of hosts for production"
+ echo " stage A predefined list of hosts for staging"
+ echo " -h A single host address"
+ echo
+ exit $1
+}
+
+# $1: Image argument
+parseImageArg() {
+ if [ -z "$1" ]; then
+ echo $MESSAGE_MISSING_OR_INVALID_ARGS
+ printUsageAndExit 1
+ fi
+ IMAGE=$1
+}
+
+parseArgs() {
+ if [ $1 == "-h" ]; then
+ parseImageArg $3
+ HOSTS=$2
+ else
+ if [ $1 == "prod" ]; then
+ if [ -z "$2" ]; then
+ IMAGE=$DEFAULT_IMAGE_PROD
+ else
+ parseImageArg $2
+ fi
+ HOSTS=$( getAWSHosts "prod" )
+ elif [ $1 == "stage" ]; then
+ if [ -z "$2" ]; then
+ IMAGE=$DEFAULT_IMAGE_STAGE
+ else
+ parseImageArg $2
+ fi
+ HOSTS=$( getAWSHosts "stage" )
+ else
+ echo
+ echo $MESSAGE_MISSING_OR_INVALID_ARGS
+ printUsageAndExit 1
+ fi
+ fi
+}
+
+# $1: Host IP
+# $2: Image
+# $3: Container name
+# $4: Container port
+runContainer() {
+ $DOCKER_CMD --tlsverify=false -H tcp://$1 run \
+ -de ENV=production \
+ -e HUB_API_BASE_URL=$HUB_GATEWAY \
+ -e REGISTRY_API_BASE_URL=$HUB_GATEWAY \
+ -e SERVICE_NAME=$HUB_SERVICE_NAME \
+ -e SERVICE_80_NAME=$HUB_SERVICE_NAME \
+ -e NEW_RELIC_LICENSE_KEY=$NEW_RELIC_LICENSE_KEY \
+ -e NEW_RELIC_APP_NAME=$NEW_RELIC_APP_NAME \
+ -e PORT=80 \
+ -p $4:80 \
+ --restart "unless-stopped" \
+ --name $3 \
+ $2
+}
+
+# $1: Host IP
+# $2: Container name
+removeContainer() {
+ $DOCKER_CMD --tlsverify=false -H tcp://$1 stop $2
+ $DOCKER_CMD --tlsverify=false -H tcp://$1 rm $2
+}
+
+# $1: Host IP
+# $2: Image name
+pullImage() {
+ $DOCKER_CMD --tlsverify=false -H tcp://$1 pull $2
+}
+
+# $1: Host IP
+# $2: Image
+deployHost() {
+ echo
+ echo "Starting to deploy ${YELLOW}$IMAGE${NO_COLOR} to ${YELLOW}$1${NO_COLOR}"
+
+ pullImage $1 $2
+
+ removeContainer $1 "hub_2_0"
+ runContainer $1 $2 "hub_2_0" 6600
+
+ removeContainer $1 "hub_2_1"
+ runContainer $1 $2 "hub_2_1" 6601
+
+ removeContainer $1 "hub_2_2"
+ runContainer $1 $2 "hub_2_2" 6602
+}
+
+# Prerequisites:
+# 1- AWS
+type aws >/dev/null 2>&1 || { echo >&2 "AWS client is required. Make sure 'aws' command is available:\nhttp://docs.aws.amazon.com/cli/latest/userguide/installing.html"; exit 1; }
+# 2- JQ
+type jq >/dev/null 2>&1 || { echo >&2 "jq JSON processor is required. Make sure 'jq' command is available:\nbrew install jq"; exit 1; }
+
+# Case for no paremeters specified
+if [ -z "$1" ]
+ then
+ echo
+ echo $MESSAGE_MISSING_OR_INVALID_ARGS
+ printUsageAndExit 1
+fi
+
+parseArgs "$@"
+
+echo
+echo "Image: ${YELLOW}$IMAGE ${NO_COLOR}"
+echo "Hosts: ${YELLOW}$HOSTS${NO_COLOR}"
+echo
+read -p "Do you want to proceed? [Y/n]" -s -n 1 KEY
+echo
+if [[ ! $KEY =~ ^[Yy]$ ]]; then
+ exit 1
+fi
+
+# Run deployment for each host
+for HUB_HOST in $HOSTS
+do
+ deployHost $HUB_HOST $IMAGE
+ echo
+ echo "Sleeping for 10 seconds to let the containers boot up..."
+ echo
+ sleep 10
+done
+
+echo
+echo "${GREEN}All done!${NO_COLOR}"
+echo
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000..c1257d728f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,28 @@
+dnsmasq:
+ build: ./containers/dnsmasq
+ ports:
+ - "53:53/udp"
+ environment:
+ - DOCKER_HOST
+haproxy:
+ build: ./containers/haproxy
+ environment:
+ - DOCKER_HOST
+ ports:
+ - "80:80"
+ - "443:443"
+hub:
+ build: .
+ command: node --harmony ./server.js
+ working_dir: /opt/hub/app/.build
+ volumes:
+ - .:/opt/hub
+ - ./private-deps/docker-ux:/opt/node_modules/docker-ux
+ - ./private-deps/hub-js-sdk:/opt/node_modules/hub-js-sdk
+ ports:
+ - "7001:3000"
+ environment:
+ DEBUG: "hub:*"
+ HUB_API_BASE_URL: "https://hub-beta-stage.docker.com"
+ REGISTRY_API_BASE_URL: "https://hub-beta-stage.docker.com"
+ ENV: development
diff --git a/dockerfiles/Dockerfile-node-alpine b/dockerfiles/Dockerfile-node-alpine
new file mode 100644
index 0000000000..5bfe426dc3
--- /dev/null
+++ b/dockerfiles/Dockerfile-node-alpine
@@ -0,0 +1,25 @@
+FROM gliderlabs/alpine:3.2
+
+ENV VERSION=v4.1.2 CMD=node DOMAIN=nodejs.org CFLAGS="-D__USE_MISC"
+# ENV VERSION=v2.2.1 CMD=iojs DOMAIN=iojs.org NO_NPM_UPDATE=true
+
+# For base builds
+ENV CONFIG_FLAGS="--without-npm" RM_DIRS=/usr/include
+# ENV CONFIG_FLAGS="--fully-static --without-npm" DEL_PKGS="libgcc libstdc++" RM_DIRS=/usr/include
+
+RUN apk-install curl make gcc g++ python linux-headers paxctl libgcc libstdc++ && \
+ curl -sSL https://${DOMAIN}/dist/${VERSION}/${CMD}-${VERSION}.tar.gz | tar -xz && \
+ cd /${CMD}-${VERSION} && \
+ ./configure --prefix=/usr ${CONFIG_FLAGS} && \
+ make -j$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) && \
+ make install && \
+ paxctl -cm /usr/bin/${CMD} && \
+ cd / && \
+ if [ -x /usr/bin/npm -a -z "$NO_NPM_UPDATE" ]; then \
+ npm install -g npm && \
+ find /usr/lib/node_modules/npm -name test -o -name .bin -type d | xargs rm -rf; \
+ fi && \
+ apk del curl make gcc g++ python linux-headers paxctl ${DEL_PKGS} && \
+ rm -rf /etc/ssl /${CMD}-${VERSION} ${RM_DIRS} \
+ /usr/share/man /tmp/* /root/.npm /root/.node-gyp \
+ /usr/lib/node_modules/npm/man /usr/lib/node_modules/npm/doc /usr/lib/node_modules/npm/html
diff --git a/dockerfiles/Dockerfile-prod-build b/dockerfiles/Dockerfile-prod-build
new file mode 100644
index 0000000000..98c39b856f
--- /dev/null
+++ b/dockerfiles/Dockerfile-prod-build
@@ -0,0 +1,22 @@
+FROM bagel/universe:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+ENV ENV production
+ENV NODE_ENV production
+
+COPY ./app /opt/hub/app
+COPY ./Makefile /opt/hub/Makefile
+COPY ./_webpack /opt/hub/_webpack
+COPY ./gulpfile.js /opt/hub/gulpfile.js
+COPY ./gulp-tasks /opt/hub/gulp-tasks
+COPY ./app-server /opt/hub/app-server
+COPY ./.eslintrc /opt/hub/.eslintrc
+
+RUN make server-prod-target
+RUN make server-extras
+RUN make js-prod
+RUN make images-prod
+RUN make docker-font-prod
+RUN gulp images::prod
+RUN make styles-base-prod
+RUN make stats-dir
+RUN make css-stats
diff --git a/dockerfiles/Dockerfile-stage-build b/dockerfiles/Dockerfile-stage-build
new file mode 100644
index 0000000000..113fe30cd3
--- /dev/null
+++ b/dockerfiles/Dockerfile-stage-build
@@ -0,0 +1,22 @@
+FROM bagel/universe:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+ENV ENV staging
+ENV NODE_ENV staging
+
+COPY ./app /opt/hub/app
+COPY ./Makefile /opt/hub/Makefile
+COPY ./_webpack /opt/hub/_webpack
+COPY ./gulpfile.js /opt/hub/gulpfile.js
+COPY ./gulp-tasks /opt/hub/gulp-tasks
+COPY ./app-server /opt/hub/app-server
+COPY ./.eslintrc /opt/hub/.eslintrc
+
+RUN make server-prod-target
+RUN make server-extras
+RUN make js-stage
+RUN make images-prod
+RUN make docker-font-prod
+RUN gulp images::prod
+RUN make styles-base-prod
+RUN make stats-dir
+RUN make css-stats
diff --git a/dockerfiles/milky-way b/dockerfiles/milky-way
new file mode 100644
index 0000000000..2dc5b9c516
--- /dev/null
+++ b/dockerfiles/milky-way
@@ -0,0 +1,10 @@
+FROM node:4.1.2
+
+WORKDIR /opt/hub
+ENV PATH /opt/hub/node_modules/.bin/:$PATH
+
+RUN apt-get update
+COPY ./private-deps /opt/hub/private-deps
+COPY ./package.json /opt/hub/
+ADD ./node_modules /opt/hub/node_modules
+#RUN npm install --production
diff --git a/dockerfiles/milky-way-no-bin b/dockerfiles/milky-way-no-bin
new file mode 100644
index 0000000000..38a9898e44
--- /dev/null
+++ b/dockerfiles/milky-way-no-bin
@@ -0,0 +1,7 @@
+FROM bagel/milky-way:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+RUN rm -rf /opt/hub/node_modules/.bin && \
+ ls && \
+ tar -czf modules.tar ./node_modules/*
+
+CMD ["cat", "/opt/hub/modules.tar"]
diff --git a/dockerfiles/saas-config b/dockerfiles/saas-config
new file mode 100644
index 0000000000..c8db476d11
--- /dev/null
+++ b/dockerfiles/saas-config
@@ -0,0 +1,29 @@
+FROM bagel/universe:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+# Source
+COPY ./app /opt/hub/app
+# Webpack
+COPY ./webpack.config.js /opt/hub/webpack.config.js
+COPY ./_webpack /opt/hub/_webpack
+# Make
+COPY ./Makefile /opt/hub/Makefile
+# Gulp
+COPY ./gulpfile.js /opt/hub/gulpfile.js
+COPY ./gulp-tasks /opt/hub/gulp-tasks
+# ESLint
+COPY ./.eslintrc /opt/hub/.eslintrc
+# Flow
+ENV LOGNAME bagels
+COPY ./flow-libs /opt/hub/flow-libs
+COPY .flowconfig /opt/hub/.flowconfig
+ENV PATH /opt/flow/:$PATH
+
+RUN npm install
+RUN DEBUG=* ENV=local webpack -d
+RUN make server-target
+RUN make styles-base
+RUN gulp images::dev
+RUN make images
+RUN make docker-font-dev
+# favicon
+COPY ./app/favicon.ico /opt/hub/app/.build/
diff --git a/dockerfiles/universe b/dockerfiles/universe
new file mode 100644
index 0000000000..638098c2c4
--- /dev/null
+++ b/dockerfiles/universe
@@ -0,0 +1,13 @@
+FROM bagel/milky-way:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+ENV NODE_ENV development
+
+RUN apt-get install libelf-dev unzip -y
+
+RUN cd /opt && wget http://flowtype.org/downloads/flow-linux64-latest.zip && unzip flow-linux64-latest.zip && rm flow-linux64-latest.zip
+
+# npm global deps
+RUN npm install -g gulp jest-cli
+
+# npm deps
+RUN cd /opt/hub && npm install
diff --git a/docs/Dockerfile b/docs/Dockerfile
new file mode 100644
index 0000000000..ca1208d173
--- /dev/null
+++ b/docs/Dockerfile
@@ -0,0 +1,8 @@
+FROM docs/base:oss
+MAINTAINER Docker Docs
+
+ENV PROJECT=docker-hub
+
+COPY . /src
+RUN rm -rf /docs/content/$PROJECT/
+COPY . /docs/content/$PROJECT/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000000..d05c6a9cf9
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,119 @@
+.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate
+
+# env vars passed through directly to Docker's build scripts
+# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily
+# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these
+DOCKER_ENVS := \
+ -e BUILDFLAGS \
+ -e DOCKER_CLIENTONLY \
+ -e DOCKER_EXECDRIVER \
+ -e DOCKER_GRAPHDRIVER \
+ -e TESTDIRS \
+ -e TESTFLAGS \
+ -e TIMEOUT
+# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds
+
+# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs)
+DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/docker-hub/)
+
+# to allow `make DOCSPORT=9000 docs`
+DOCSPORT := 8000
+
+# Get the IP ADDRESS
+DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''")
+HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)")
+HUGO_BIND_IP=0.0.0.0
+
+GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
+DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH))
+DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH))
+
+
+DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE
+
+# for some docs workarounds (see below in "docs-build" target)
+GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
+
+default: docs
+
+test: docs-build
+ $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 \
+ -v $(CURDIR):/docs/content/docker-hub/ \
+ -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" \
+ hugo server \
+ --log=true --watch=true \
+ --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
+
+
+docs: docs-build
+ $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
+
+docs-draft: docs-build
+ $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
+
+
+docs-shell: docs-build
+ $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash
+
+
+docs-build:
+# ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files
+# echo "$(GIT_BRANCH)" > GIT_BRANCH
+# echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET
+# echo "$(GITCOMMIT)" > GITCOMMIT
+ docker build -t "$(DOCKER_DOCS_IMAGE)" .
+
+# use screenshot container to update screenshots
+NOAUTHSCREENSHOT := docker run -it --rm --name screen -v $(CURDIR):/srv --env-file=noauthpasswords.env svendowideit/screenshot
+SCREENSHOT := docker run -it --rm --name screen -v $(CURDIR):/srv --env-file=passwords.env svendowideit/screenshot
+NOLINKS := docker run -it --rm --name screen -v $(CURDIR):/srv --env-file=passwords.env --env-file=nolinks.env svendowideit/screenshot
+GITHUB_DOCSUSER := docker run -it --rm --name screen -v $(CURDIR):/srv --env-file=passwords.env --env-file=githubdocs.env svendowideit/screenshot
+
+# testing
+testimage:
+ #$(NOAUTHSCREENSHOT) https://hub-beta.docker.com/ images/register-web.png 1280px
+ #$(SCREENSHOT) https://hub-beta.docker.com/explore/ images/dashboard.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/organizations/ orgs.png 1280px
+
+t2:
+ $(NOAUTHSCREENSHOT) https://hub-beta.docker.com/ images/register-web.png 1280px
+
+docs-images:
+ # non-authenticated
+ $(NOAUTHSCREENSHOT) https://hub-beta.docker.com/ images/register-web.png 1280px
+ $(NOAUTHSCREENSHOT) https://hub-beta.docker.com/login/ images/login-web.png 1280px
+ # authenticated
+ $(SCREENSHOT) https://hub-beta.docker.com/explore/ images/dashboard.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/organizations/ images/orgs.png 1280px
+ # $(SCREENSHOT) https://hub-beta.docker.com/ images/deploy_key.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/r/docsorg/private/~/settings/collaborators/ images/org-repo-collaborators.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/repos.png 1280px
+ # $(SCREENSHOT) https://hub-beta.docker.com/ images/invite.png 1280px
+ # $(SCREENSHOT) https://hub-beta.docker.com/ images/build-trigger.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/hub.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/u/docsorg/dashboard/teams/boomteam/ images/groups.png 1280px
+ $(SCREENSHOT) https://hub-beta.docker.com/r/library/busybox/tags/# images/busybox-image-tags.png 1280px*600px
+ # docs/images/getting-started.png (needs a new empty account)
+
+nolinks:
+ # bitbucket.md, github.md
+ # uses the `nolinks` user: an account that has no accounts linked
+ $(NOLINKS) https://hub-beta.docker.com/account/authorized-services/ images/authorized-services.png 1280px
+ $(NOLINKS) https://hub-beta.docker.com/account/authorized-services/github-permissions/ add-authorized-github-service.png 1280px
+
+# BROKEN
+github:
+ $(GITHUB_DOCSUSER) https://github.com/docsuser/private/settings/hooks github-side-hook.png 1280px
+
+# BROKEN, wrong URL and needs hand editing to capture the specific UI elements
+broken-gitimages:
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_settings.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_menu.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_add_ssh_user_key.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_team_members.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh-check-user-org-dh-app-access.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_service_hook.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh-check-admin-org-dh-app-access.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_org_members.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_docker-service.png
+ $(SCREENSHOT) https://hub-beta.docker.com/ images/gh_repo_deploy_key.png
diff --git a/docs/accounts.md b/docs/accounts.md
new file mode 100644
index 0000000000..f58acdef80
--- /dev/null
+++ b/docs/accounts.md
@@ -0,0 +1,57 @@
++++
+title = "Your Docker ID"
+description = "Your Docker ID"
+keywords = ["Docker, docker, trusted, sign-up, registry, accounts, plans, Dockerfile, Docker Hub, docs, documentation"]
+[menu.main]
+parent="mn_pubhub"
+weight=-90
++++
+
+# Your Docker ID
+
+You can `search` for Docker images and `pull` them from [Docker
+Hub](https://hub.docker.com) without signing in or even having an
+account. However, to `push` images, leave comments, or to *star*
+a repository, you need a free [Docker ID](https://hub.docker.com) to log in to Docker Hub.
+
+Once you have a personal Docker ID, you can also create or join
+Docker Hub [Organizations and Teams](orgs.md).
+
+## Register for a Docker ID
+
+If you're not already logged in, go to [Docker Hub](https://hub.docker.com)
+to use the sign up page.
+A valid email address is required to register. A verification email is sent to this address to activate your account.
+
+You cannot log in to your Docker ID until you verify the email address.
+
+#### Confirm your email
+
+Once you've filled in the registration form, check your email for a welcome message asking for
+confirmation so we can activate your account.
+
+## Login
+
+After you complete the account creation process, you can log in any time using the web console with your Docker ID:
+
+
+
+Or via the command line with the `docker login` command:
+
+ $ docker login
+
+Your Docker ID is now active and ready to use.
+
+> **Note:**
+> Your authentication credentials will be stored in the `.dockercfg`
+> authentication file in your home directory.
+
+### Upgrading your account
+
+Free Hub accounts include one private registry. If you need more private registries, you can [upgrade your account](https://hub.docker.com/account/billing-plans/) to a paid plan directly from the Hub.
+
+## Password reset process
+
+If you can't access your account for some reason, you can reset your password
+from the [*Password Reset*](https://hub.docker.com/reset-password/)
+page.
diff --git a/docs/bitbucket.md b/docs/bitbucket.md
new file mode 100644
index 0000000000..7bd6d8ec27
--- /dev/null
+++ b/docs/bitbucket.md
@@ -0,0 +1,50 @@
++++
+title = "Automated Builds with Bitbucket"
+description = "Docker Hub Automated Builds using Bitbucket"
+keywords = ["Docker, docker, registry, accounts, plans, Dockerfile, Docker Hub, docs, documentation, trusted, builds, trusted builds, automated builds, bitbucket"]
+[menu.main]
+parent="mn_pubhub"
+weight=8
++++
+
+# Automated Builds with Bitbucket
+
+If you've previously linked Docker Hub to your Bitbucket account,
+you'll be able to skip to [Creating an Automated Build](#creating-an-automated-build).
+
+## Linking to your Bitbucket account
+
+In order to set up an Automated Build of a repository on Bitbucket, you need to
+link your [Docker Hub](https://hub.docker.com/account/authorized-services/)
+account to a Bitbucket account. This will allow the registry to see your Bitbucket
+repositories.
+
+To add, remove or view your linked account, go to the "Linked Accounts & Services"
+section of your Hub profile "Settings".
+
+
+
+Then follow the onscreen instructions to authorize and link your
+Bitbucket account to Docker Hub. Once it is linked, you'll be able
+to create a Docker Hub repository from which to create the Automatic Build.
+
+## Creating an Automated Build
+
+You can [create an Automated Build](
+https://hub.docker.com/add/automated-build/bitbucket/orgs/) from any of your
+public or private Bitbucket repositories with a `Dockerfile`.
+
+To get started, log in to Docker Hub and click the
+"Create ▼" menu item at the top right of the screen. Then select
+[Create Automated Build](https://hub.docker.com/add/automated-build).
+
+Select the the linked Bitbucket account, and then choose a repository to set up
+an Automated Build for.
+
+## The Bitbucket webhook
+
+When you create an Automated Build in Docker Hub, a webhook is added to your Bitbucket repository automatically.
+
+You can also manually add a webhook from your repository's **Settings** page. Set the URL to `https://registry.hub.docker.com/hooks/bitbucket`, to be triggered for repository pushes.
+
+
diff --git a/docs/builds.md b/docs/builds.md
new file mode 100644
index 0000000000..199c437342
--- /dev/null
+++ b/docs/builds.md
@@ -0,0 +1,219 @@
++++
+title = "Automated Builds"
+description = "Docker Hub Automated Builds"
+keywords = ["Dockerfile, Hub, builds, trusted builds, automated builds"]
+[menu.main]
+parent="mn_pubhub"
+weight=6
++++
+
+# Automated Builds on Docker Hub
+
+You can build your images automatically from a build context stored in a repository. A *build context* is a Dockerfile and any files at a specific location. For an automated build, the build context is a repository containing a Dockerfile.
+
+Automated Builds have several advantages:
+
+ * Images built in this way are built exactly as specified.
+ * The `Dockerfile` is available to anyone with access to
+your Docker Hub repository.
+ * Your repository is kept up-to-date with code changes automatically.
+
+Automated Builds are supported for both public and private repositories
+on both [GitHub](http://github.com) and [Bitbucket](https://bitbucket.org/). This document guides you through the process of working with automated builds.
+
+## Prerequisites
+
+To use automated builds you must have an [account on Docker Hub](accounts.md) and on the hosted repository provider (GitHub or Bitbucket). If
+you have previously linked your Github or Bitbucket account, you must have
+chosen the Public and Private connection type. To view your current connection
+settings, log in to Docker Hub and choose Profile > Settings > Linked Accounts & Services.
+
+
+## Link to a hosted repository service
+
+1. Log into Docker Hub.
+
+2. Navigate to Profile > Settings > Linked Accounts & Services.
+
+3. Click the service you want to link.
+
+ The system prompts you to choose between Public and Private and Limited Access. The Public and Private connection type is required if you want to use the Automated Builds.
+
+4. Press Select under Public and Private connection type.
+
+ The system prompts you to enter your service credentials (Bitbucket or GitHub) to login. For example, Bitbucket's prompt looks like this:
+
+ 
+
+ After you grant access to your code repository, the system returns you to Docker Hub and the link is complete.
+
+ 
+
+## Create an automated build
+
+Automated build repositories rely on the integration with your code repository
+in order to build. However, you can also push already-built images to these
+repositories using the `docker push` command.
+
+1. Select **Create** > **Create Automated Build** from Docker Hub.
+
+ The system prompts you with a list of User/Organizations and code repositories.
+
+2. Select from the User/Organizations.
+
+3. Optionally, type to filter the repository list.
+
+4. Pick the project to build.
+
+ The system displays the Create Automated Build dialog.
+
+ 
+
+ The dialog assumes some defaults which you can customize. By default, Docker
+ builds images for each branch in your repository. It assumes the Dockerfile
+ lives at the root of your source. When it builds an image, Docker tags it with
+ the branch name.
+
+6. Customize the automated build by pressing the Click here to customize this behavior link.
+
+ 
+
+ Specify which code branches or tags to build from. You can add new
+ configurations by clicking the + (plus sign). The dialog accepts regular
+ expressions.
+
+ 
+
+9. Click Create.
+
+ The system displays the home page for your AUTOMATED BUILD.
+
+ 
+
+ Within GitHub, a Docker integration appears in your repositories Settings > Webhooks & services page.
+
+ 
+
+ A similar page appears in Bitbucket if you use that code repository.Be
+ careful to leave the Docker integration in place. Removing it causes your
+ automated builds to stop.
+
+### Understand the build process
+
+The first time you create a new automated build, Docker Hub builds your image.
+In a few minutes, you should see your new build on the image dashboard. The
+Build Details page shows a log of your build systems:
+
+
+
+During the build process, Docker copies the contents of your `Dockerfile` to
+Docker Hub. The Docker community (for public repositories) or approved team
+members/orgs (for private repositories) can then view the Dockerfile on your
+repository page.
+
+The build process looks for a `README.md` in the same directory as your
+`Dockerfile`. If you have a `README.md` file in your repository, it is used in
+the repository as the full description. If you change the full description after
+a build, it's overwritten the next time the Automated Build runs. To make
+changes, modify the `README.md` in your Git repository.
+
+You can only trigger one build at a time and no more than one every five
+minutes. If you already have a build pending, or if you recently submitted a
+build request, Docker ignores new requests.
+
+### Build statuses explained
+
+Check your build status through the Build Details screen as seen in the following example.
+
+
+
+The statuses are:
+
+* **Queued**: You're in line and your image will be built soon. Queue time varies depending on number of concurrent builds available to you.
+* **Building**: Your image is currently being constructed.
+* **Success**: The image has been built with no issues.
+* **Error**: There was an issue with your image. Click the row to access the Builds Details screen. The banner at the top of the page displays the last sentence of the log file indicating what the error was. If you need more information, scroll to the bottom of the screen to the logs section.
+
+
+## Use the Build Settings page
+
+The Build Settings page allows you to manage your existing automated build configurations and add new ones. By default, when new code is merged into your source repository, it triggers a build of your DockerHub image.
+
+
+
+Clear the checkbox to turn this behavior off. You can use the other settings on
+the page to configure and build images.
+
+## Add and run a new build
+
+At the top of the Build Dialog is a list of configured builds. You can build from a code branch or by build tag.
+
+
+
+Docker builds everything listed whenever a push is made to the code repository.
+If you specify a branch or tag, you can manually build that image by
+pressing the Trigger. If you use a regular expression syntax (regex) to define
+your build branch or tag, Docker does not give you the option to manually build.
+To add a new build:
+
+1. Press the + (plus sign).
+
+2. Choose the Type.
+
+ You can build by a code branch or by an image tag.
+
+3. Enter the Name of the branch or tag.
+
+ You can enter a specific value or use a regex to select multiple values. To
+ see examples of regex, press the Show More link on the right of the page.
+
+ 
+
+4. Enter a Dockerfile location.
+
+5. Specify a Tag Name.
+
+6. Press Save Changes.
+
+If you make a mistake or want to delete a build, press the - (minus sign) and then Save Changes.
+
+## Repository links
+
+Repository links let you link one Automated Build with another. If one Automated
+Build gets updated, Docker triggers a build of the other. This makes it easy to
+ensure that related images are kept in sync. You can link more than one image
+repository. You only need to link one side of two related builds. Linking both
+sides causes an endless build loop.
+
+To add a link:
+
+1. Go to the Build Settings for an automated build repository.
+
+2. In the Repository Links section, enter an image repository name.
+
+ A remote repository name should be either an official repository name such as `ubuntu` or a public repository name `namespace/repoName`.
+
+3. Press Add.
+
+ 
+
+
+## Remote Build triggers
+
+To trigger Automated Builds programmatically, you can set up a remote build
+trigger in another application such as GitHub or Bitbucket. When you Activate
+the build trigger for an Automated Build, it supplies you with a Token and a URL.
+
+
+
+You can use `curl` to trigger a build:
+
+```bash
+$ curl --data build=true -X POST https://registry.hub.docker.com/u/svendowideit/testhook/trigger/be579c
+82-7c0e-11e4-81c4-0242ac110020/
+OK
+```
+
+To verify everything is working correctly, check the **Last 10 Trigger Logs** on the page.
+
+
diff --git a/docs/github.md b/docs/github.md
new file mode 100644
index 0000000000..797c37cc9c
--- /dev/null
+++ b/docs/github.md
@@ -0,0 +1,204 @@
++++
+title = "Automated Builds from GitHub"
+description = "Docker Hub Automated Builds with GitHub"
+keywords = ["Docker, docker, registry, accounts, plans, Dockerfile, Docker Hub, docs, documentation, trusted, builds, trusted builds, automated builds, GitHub"]
+[menu.main]
+parent="mn_pubhub"
+weight=9
++++
+
+# Automated Builds from GitHub
+
+If you've previously linked Docker Hub to your GitHub account,
+you'll be able to skip to [Creating an Automated Build](#creating-an-automated-build).
+
+## Linking Docker Hub to a GitHub account
+
+> *Note:*
+> Automated Builds currently require *read* and *write* access since
+> [Docker Hub](https://hub.docker.com) needs to set up a GitHub service
+> hook. We have no choice here, this is how GitHub manages permissions.
+> We do guarantee nothing else will be touched in your account.
+
+In order to set up an Automated Build of a repository on GitHub, you need to
+link [Docker Hub](https://hub.docker.com/account/authorized-services/) to your GitHub account. This will allow the registry to see your GitHub
+repositories.
+
+To add, remove or view your linked account, go to the "Linked Accounts & Services" section of your Hub profile "Settings".
+
+
+
+When linking to GitHub, you'll need to select either "Public and Private",
+or "Limited Access" linking.
+
+
+
+The "Public and Private" option is the easiest to use,
+as it grants the Docker Hub full access to all of your repositories. GitHub
+also allows you to grant access to repositories belonging to your GitHub
+organizations.
+
+If you choose "Limited Access", Docker Hub only gets permission
+to access your public data and public repositories.
+
+Follow the onscreen instructions to authorize and link your
+GitHub account to Docker Hub. Once it is linked, you'll be able to
+choose a source repository from which to create the Automatic Build.
+
+You will be able to review and revoke Docker Hub's access by visiting the
+[GitHub User's Applications settings](https://github.com/settings/applications).
+
+> **Note**: If you delete the GitHub account linkage that is used for one of your
+> automated build repositories, the previously built images will still be available.
+> If you re-link to that GitHub account later, the automated build can be started
+> using the "Start Build" button on the Hub, or if the webhook on the GitHub repository
+> still exists, it will be triggered by any subsequent commits.
+
+## Auto builds and limited linked GitHub accounts.
+
+If you selected to link your GitHub account with only a "Limited Access" link, then
+after creating your automated build, you will need to either manually trigger a
+Docker Hub build using the "Start a Build" button, or add the GitHub webhook
+manually, as described in [GitHub Service Hooks](#github-service-hooks).
+
+## Changing the GitHub user link
+
+If you want to remove, or change the level of linking between your GitHub account
+and the Docker Hub, you need to do this in two places.
+
+First, remove the "Linked Account" from your Docker Hub "Settings".
+Then go to your GitHub account's Personal settings, and in the "Applications"
+section, "Revoke access".
+
+You can now re-link your account at any time.
+
+## GitHub organizations
+
+GitHub organizations and private repositories forked from organizations will be
+made available to auto build using the "Docker Hub Registry" application, which
+needs to be added to the organization - and then will apply to all users.
+
+To check, or request access, go to your GitHub user's "Setting" page, select the
+"Applications" section from the left side bar, then click the "View" button for
+"Docker Hub Registry".
+
+
+
+The organization's administrators may need to go to the Organization's "Third
+party access" screen in "Settings" to grant or deny access to the Docker Hub
+Registry application. This change will apply to all organization members.
+
+
+
+More detailed access controls to specific users and GitHub repositories can be
+managed using the GitHub "People and Teams" interfaces.
+
+## Creating an Automated Build
+
+You can [create an Automated Build](
+https://hub.docker.com/add/automated-build/github/) from any of your
+public or private GitHub repositories that have a `Dockerfile`.
+
+Once you've selected the source repository, you can then configure:
+
+- The Hub user/org namespace the repository is built to - either your Docker ID name, or the name of any Hub organizations your account is in
+- The Docker repository name the image is built to
+- The description of the repository
+- If the visibility of the Docker repository: "Public" or "Private"
+ You can change the accessibility options after the repository has been created.
+ If you add a Private repository to a Hub user namespace, then you can only add other users
+ as collaborators, and those users will be able to view and pull all images in that
+ repository. To configure more granular access permissions, such as using teams of
+ users or allow different users access to different image tags, then you need
+ to add the Private repository to a Hub organization for which your user has Administrator
+ privileges.
+- Enable or disable rebuilding the Docker image when a commit is pushed to the
+ GitHub repository.
+
+You can also select one or more:
+- The git branch/tag,
+- A repository sub-directory to use as the context,
+- The Docker image tag name
+
+You can modify the description for the repository by clicking the "Description" section
+of the repository view.
+Note that the "Full Description" will be over-written by the README.md file when the
+next build is triggered.
+
+## GitHub private submodules
+
+If your GitHub repository contains links to private submodules, you'll get an
+error message in your build.
+
+Normally, the Docker Hub sets up a deploy key in your GitHub repository.
+Unfortunately, GitHub only allows a repository deploy key to access a single repository.
+
+To work around this, you can create a dedicated user account in GitHub and attach
+the automated build's deploy key that account. This dedicated build account
+can be limited to read-only access to just the repositories required to build.
+
+
+
+
+
Step
+
Screenshot
+
Description
+
+
+
+
+
1.
+
+
First, create the new account in GitHub. It should be given read-only
+ access to the main repository and all submodules that are needed.
+
+
+
2.
+
+
This can be accomplished by adding the account to a read-only team in
+ the organization(s) where the main GitHub repository and all submodule
+ repositories are kept.
+
+
+
3.
+
+
Next, remove the deploy key from the main GitHub repository. This can be done in the GitHub repository's "Deploy keys" Settings section.
+
+
+
4.
+
+
Your automated build's deploy key is in the "Build Details" menu
+ under "Deploy keys".
+
+
+
5.
+
+
In your dedicated GitHub User account, add the deploy key from your
+ Docker Hub Automated Build.
+
+
+
+
+## GitHub service hooks
+
+A GitHub Service hook allows GitHub to notify the Docker Hub when something has
+been committed to a given git repository.
+
+When you create an Automated Build from a GitHub user that has full "Public and
+Private" linking, a Service Hook should get automatically added to your GitHub
+repository.
+
+If your GitHub account link to the Docker Hub is "Limited Access", then you will
+need to add the Service Hook manually.
+
+To add, confirm, or modify the service hook, log in to GitHub, then navigate to
+the repository, click "Settings" (the gear), then select "Webhooks & Services".
+You must have Administrator privileges on the repository to view or modfy
+this setting.
+
+The image below shows the "Docker" Service Hook.
+
+
+
+If you add the "Docker" service manually, make sure the "Active" checkbox is
+selected and click the "Update service" button to save your changes.
diff --git a/docs/images/add-authorized-github-service.png b/docs/images/add-authorized-github-service.png
new file mode 100644
index 0000000000..a4fd351713
Binary files /dev/null and b/docs/images/add-authorized-github-service.png differ
diff --git a/docs/images/authorized-services.png b/docs/images/authorized-services.png
new file mode 100644
index 0000000000..ccae6a7256
Binary files /dev/null and b/docs/images/authorized-services.png differ
diff --git a/docs/images/bitbucket-hook.png b/docs/images/bitbucket-hook.png
new file mode 100644
index 0000000000..3fd37708d8
Binary files /dev/null and b/docs/images/bitbucket-hook.png differ
diff --git a/docs/images/bitbucket_creds.png b/docs/images/bitbucket_creds.png
new file mode 100644
index 0000000000..b24e185268
Binary files /dev/null and b/docs/images/bitbucket_creds.png differ
diff --git a/docs/images/build-by.png b/docs/images/build-by.png
new file mode 100644
index 0000000000..d1071da272
Binary files /dev/null and b/docs/images/build-by.png differ
diff --git a/docs/images/build-states-ex.png b/docs/images/build-states-ex.png
new file mode 100644
index 0000000000..8f068ddd4d
Binary files /dev/null and b/docs/images/build-states-ex.png differ
diff --git a/docs/images/build-trigger.png b/docs/images/build-trigger.png
new file mode 100644
index 0000000000..8f034608ae
Binary files /dev/null and b/docs/images/build-trigger.png differ
diff --git a/docs/images/busybox-image-tags.png b/docs/images/busybox-image-tags.png
new file mode 100644
index 0000000000..c3b07adb5e
Binary files /dev/null and b/docs/images/busybox-image-tags.png differ
diff --git a/docs/images/create-dialog.png b/docs/images/create-dialog.png
new file mode 100644
index 0000000000..1a4bddaf9b
Binary files /dev/null and b/docs/images/create-dialog.png differ
diff --git a/docs/images/create-dialog1.png b/docs/images/create-dialog1.png
new file mode 100644
index 0000000000..c14e099f25
Binary files /dev/null and b/docs/images/create-dialog1.png differ
diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png
new file mode 100644
index 0000000000..038be4cbba
Binary files /dev/null and b/docs/images/dashboard.png differ
diff --git a/docs/images/deploy_key.png b/docs/images/deploy_key.png
new file mode 100644
index 0000000000..f1d8d92d22
Binary files /dev/null and b/docs/images/deploy_key.png differ
diff --git a/docs/images/docker-integration.png b/docs/images/docker-integration.png
new file mode 100644
index 0000000000..362e27ac09
Binary files /dev/null and b/docs/images/docker-integration.png differ
diff --git a/docs/images/first_pending.png b/docs/images/first_pending.png
new file mode 100644
index 0000000000..9deaeeea49
Binary files /dev/null and b/docs/images/first_pending.png differ
diff --git a/docs/images/getting-started.png b/docs/images/getting-started.png
new file mode 100644
index 0000000000..59f242d797
Binary files /dev/null and b/docs/images/getting-started.png differ
diff --git a/docs/images/gh-check-admin-org-dh-app-access.png b/docs/images/gh-check-admin-org-dh-app-access.png
new file mode 100644
index 0000000000..0df38c6946
Binary files /dev/null and b/docs/images/gh-check-admin-org-dh-app-access.png differ
diff --git a/docs/images/gh-check-user-org-dh-app-access.png b/docs/images/gh-check-user-org-dh-app-access.png
new file mode 100644
index 0000000000..13ad6468f6
Binary files /dev/null and b/docs/images/gh-check-user-org-dh-app-access.png differ
diff --git a/docs/images/gh_add_ssh_user_key.png b/docs/images/gh_add_ssh_user_key.png
new file mode 100644
index 0000000000..7d0092170f
Binary files /dev/null and b/docs/images/gh_add_ssh_user_key.png differ
diff --git a/docs/images/gh_docker-service.png b/docs/images/gh_docker-service.png
new file mode 100644
index 0000000000..7a84c81b7e
Binary files /dev/null and b/docs/images/gh_docker-service.png differ
diff --git a/docs/images/gh_menu.png b/docs/images/gh_menu.png
new file mode 100644
index 0000000000..84458a445f
Binary files /dev/null and b/docs/images/gh_menu.png differ
diff --git a/docs/images/gh_org_members.png b/docs/images/gh_org_members.png
new file mode 100644
index 0000000000..465f5da565
Binary files /dev/null and b/docs/images/gh_org_members.png differ
diff --git a/docs/images/gh_repo_deploy_key.png b/docs/images/gh_repo_deploy_key.png
new file mode 100644
index 0000000000..983b5eec77
Binary files /dev/null and b/docs/images/gh_repo_deploy_key.png differ
diff --git a/docs/images/gh_service_hook.png b/docs/images/gh_service_hook.png
new file mode 100644
index 0000000000..c344c24afc
Binary files /dev/null and b/docs/images/gh_service_hook.png differ
diff --git a/docs/images/gh_settings.png b/docs/images/gh_settings.png
new file mode 100644
index 0000000000..2af9cb5138
Binary files /dev/null and b/docs/images/gh_settings.png differ
diff --git a/docs/images/gh_team_members.png b/docs/images/gh_team_members.png
new file mode 100644
index 0000000000..3bdf4abd95
Binary files /dev/null and b/docs/images/gh_team_members.png differ
diff --git a/docs/images/github-side-hook.png b/docs/images/github-side-hook.png
new file mode 100644
index 0000000000..c742b4080a
Binary files /dev/null and b/docs/images/github-side-hook.png differ
diff --git a/docs/images/groups.png b/docs/images/groups.png
new file mode 100644
index 0000000000..b725b48ba9
Binary files /dev/null and b/docs/images/groups.png differ
diff --git a/docs/images/home-page.png b/docs/images/home-page.png
new file mode 100644
index 0000000000..e9c66cec9a
Binary files /dev/null and b/docs/images/home-page.png differ
diff --git a/docs/images/hub.png b/docs/images/hub.png
new file mode 100644
index 0000000000..959f961ae5
Binary files /dev/null and b/docs/images/hub.png differ
diff --git a/docs/images/invite.png b/docs/images/invite.png
new file mode 100644
index 0000000000..f663340443
Binary files /dev/null and b/docs/images/invite.png differ
diff --git a/docs/images/linked-acct.png b/docs/images/linked-acct.png
new file mode 100644
index 0000000000..340733602c
Binary files /dev/null and b/docs/images/linked-acct.png differ
diff --git a/docs/images/login-web.png b/docs/images/login-web.png
new file mode 100644
index 0000000000..64e29c9014
Binary files /dev/null and b/docs/images/login-web.png differ
diff --git a/docs/images/merge_builds.png b/docs/images/merge_builds.png
new file mode 100644
index 0000000000..589bba9325
Binary files /dev/null and b/docs/images/merge_builds.png differ
diff --git a/docs/images/org-repo-collaborators.png b/docs/images/org-repo-collaborators.png
new file mode 100644
index 0000000000..3d80a1aa66
Binary files /dev/null and b/docs/images/org-repo-collaborators.png differ
diff --git a/docs/images/orgs.png b/docs/images/orgs.png
new file mode 100644
index 0000000000..fe1b89b31c
Binary files /dev/null and b/docs/images/orgs.png differ
diff --git a/docs/images/plus-carrot.png b/docs/images/plus-carrot.png
new file mode 100644
index 0000000000..8c4cd37ded
Binary files /dev/null and b/docs/images/plus-carrot.png differ
diff --git a/docs/images/prompt.png b/docs/images/prompt.png
new file mode 100644
index 0000000000..a94ccf08c9
Binary files /dev/null and b/docs/images/prompt.png differ
diff --git a/docs/images/regex-help.png b/docs/images/regex-help.png
new file mode 100644
index 0000000000..ad404de476
Binary files /dev/null and b/docs/images/regex-help.png differ
diff --git a/docs/images/register-web.png b/docs/images/register-web.png
new file mode 100644
index 0000000000..ea95e1f50b
Binary files /dev/null and b/docs/images/register-web.png differ
diff --git a/docs/images/repo_links.png b/docs/images/repo_links.png
new file mode 100644
index 0000000000..09a4bd63c1
Binary files /dev/null and b/docs/images/repo_links.png differ
diff --git a/docs/images/repos.png b/docs/images/repos.png
new file mode 100644
index 0000000000..959f961ae5
Binary files /dev/null and b/docs/images/repos.png differ
diff --git a/docs/images/scan-drilldown.gif b/docs/images/scan-drilldown.gif
new file mode 100644
index 0000000000..e74acc162e
Binary files /dev/null and b/docs/images/scan-drilldown.gif differ
diff --git a/docs/images/scan-results.png b/docs/images/scan-results.png
new file mode 100644
index 0000000000..608674fee3
Binary files /dev/null and b/docs/images/scan-results.png differ
diff --git a/docs/images/scan-tags.png b/docs/images/scan-tags.png
new file mode 100644
index 0000000000..ec2de8baad
Binary files /dev/null and b/docs/images/scan-tags.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000000..5a51c99055
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,92 @@
++++
+title = "Overview of Docker Hub"
+description = "Docker Hub overview"
+keywords = ["Docker, docker, registry, accounts, plans, Dockerfile, Docker Hub, docs, documentation, accounts, organizations, repositories, groups, teams"]
+aliases = "/docker-hub/overview/"
+[menu.main]
+parent="mn_pubhub"
+weight=-99
+
++++
+
+# Overview of Docker Hub
+
+[Docker Hub](https://hub.docker.com) is a cloud-based registry service which
+allows you to link to code repositories, build your images and test them, stores
+manually pushed images, and links to [Docker Cloud](https://docs.docker.com/docker-cloud/) so you can deploy images to your
+hosts. It provides a centralized resource for container image discovery,
+distribution and change management, [user and team collaboration](orgs.md), and
+workflow automation throughout the development pipeline.
+
+Log in to Docker Hub and Docker Cloud using [your free Docker ID](accounts.md).
+
+
+
+Docker Hub provides the following major features:
+
+* [Image Repositories](repos.md): Find, manage, and push and pull images from community, official, and private image libraries.
+* [Automated Builds](builds.md): Automatically create new images when you make changes to a source code repository.
+* [Webhooks](webhooks.md): A feature of Automated Builds, Webhooks let you trigger actions after a successful push to a repository.
+* [Organizations](orgs.md): Create work groups to manage access to image repositories.
+* GitHub and Bitbucket Integration: Add the Hub and your Docker Images to your current workflows.
+
+
+## Create a Docker ID
+
+To explore Docker Hub, you'll need to create an account by following the
+directions in [Your Docker ID](accounts.md).
+
+> **Note**: You can search for and pull Docker images from Hub without logging in, however to push images you must log in.
+
+Your Docker ID gives you one private Docker Hub repository for free. If you need
+more private repositories, you can upgrade from your free account to a paid
+plan. To learn more, log in to Docker Hub and go to [Billing & Plans](https://hub.docker.com/account/billing-plans/), in the Settings menu.
+
+### Explore repositories
+
+You can find public repositories and images from Docker Hub in two ways.
+You can "Search" from the Docker Hub website, or you can use the Docker command line tool to run the `docker search` command. For example if you were looking for an ubuntu image, you might run the following command line search:
+
+```
+ $ docker search ubuntu
+```
+
+Both methods list the available public repositories on Docker Hub which match
+the search term.
+
+Private repositories do not appear in the repository search results. To see all
+the repositories you can access and their status, view your "Dashboard" page on
+[Docker Hub](https://hub.docker.com).
+
+
+You can find more information on working with Docker images in the [Docker userguide](https://docs.docker.com/userguide/dockerimages/).
+
+### Use Official Repositories
+
+Docker Hub contains a number of [Official
+Repositories](http://hub.docker.com/explore/). These are public, certified
+repositories from vendors and contributors to Docker. They contain Docker images
+from vendors like Canonical, Oracle, and Red Hat that you can use as the basis
+to building your applications and services.
+
+With Official Repositories you know you're using an optimized and
+up-to-date image that was built by experts to power your applications.
+
+> **Note:** If you would like to contribute an Official Repository for your organization or product, see the documentation on [Official Repositories on Docker Hub](official_repos.md) for more information.
+
+
+## Work with Docker Hub image repositories
+
+Docker Hub provides a place for you and your team to build and ship Docker images.
+
+You can configure Docker Hub repositories in two ways:
+
+* [Repositories](repos.md), which allow you to push images from a local Docker daemon to Docker Hub, and
+* [Automated Builds](builds.md), which link to a source code repository and trigger an image rebuild process on Docker Hub when changes are detected in the source code.
+
+You can create public repositories which can be accessed by any other Hub user, or you can create private repositories with limited access you control.
+
+### Docker commands and Docker Hub
+
+Docker itself provides access to Docker Hub services via the [`docker search`](http://docs.docker.com/reference/commandline/search),
+[`pull`](http://docs.docker.com/reference/commandline/pull), [`login`](http://docs.docker.com/reference/commandline/login), and [`push`](http://docs.docker.com/reference/commandline/push) commands.
diff --git a/docs/menu.md b/docs/menu.md
new file mode 100644
index 0000000000..1a89dc09ae
--- /dev/null
+++ b/docs/menu.md
@@ -0,0 +1,14 @@
+
+
+# Menu topic
+
+If you can view this content, please raise a bug report.
diff --git a/docs/official_repos.md b/docs/official_repos.md
new file mode 100644
index 0000000000..56b5689a7f
--- /dev/null
+++ b/docs/official_repos.md
@@ -0,0 +1,125 @@
++++
+title = "Official Repositories on Docker Hub"
+description = "Guidelines for Official Repositories on Docker Hub"
+keywords = ["Docker, docker, registry, accounts, plans, Dockerfile, Docker Hub, docs, official, image, documentation"]
+[menu.main]
+parent="mn_pubhub"
+weight=15
++++
+
+# Official Repositories on Docker Hub
+
+The Docker [Official Repositories](https://hub.docker.com/official/) are a
+curated set of Docker repositories that are promoted on Docker Hub. They are designed to:
+
+* Provide essential base OS repositories (for example,
+ [ubuntu](https://hub.docker.com/_/ubuntu/),
+ [centos](https://hub.docker.com/_/centos/)) that serve as the
+ starting point for the majority of users.
+
+* Provide drop-in solutions for popular programming language runtimes, data
+ stores, and other services, similar to what a Platform-as-a-Service (PAAS)
+ would offer.
+
+* Exemplify [`Dockerfile` best practices](https://docs.docker.com/articles/dockerfile_best-practices)
+ and provide clear documentation to serve as a reference for other `Dockerfile`
+ authors.
+
+* Ensure that security updates are applied in a timely manner. This is
+ particularly important as many Official Repositories are some of the most
+ popular on Docker Hub.
+
+* Provide a channel for software vendors to redistribute up-to-date and
+ supported versions of their products. Organization accounts on Docker Hub can
+ also serve this purpose, without the careful review or restrictions on what
+ can be published.
+
+Docker, Inc. sponsors a dedicated team that is responsible for reviewing and
+publishing all Official Repositories content. This team works in collaboration
+with upstream software maintainers, security experts, and the broader Docker
+community.
+
+While it is preferrable to have upstream software authors maintaining their
+corresponding Official Repositories, this is not a strict requirement. Creating
+and maintaining images for Official Repositories is a public process. It takes
+place openly on GitHub where participation is encouraged. Anyone can provide
+feedback, contribute code, suggest process changes, or even propose a new
+Official Repository.
+
+## Should I use Official Repositories?
+
+New Docker users are encouraged to use the Official Repositories in their
+projects. These repositories have clear documentation, promote best practices,
+and are designed for the most common use cases. Advanced users are encouraged to
+review the Official Repositories as part of their `Dockerfile` learning process.
+
+A common rationale for diverging from Official Repositories is to optimize for
+image size. For instance, many of the programming language stack images contain
+a complete build toolchain to support installation of modules that depend on
+optimized code. An advanced user could build a custom image with just the
+necessary pre-compiled libraries to save space.
+
+A number of language stacks such as
+[python](https://hub.docker.com/_/python/) and
+[ruby](https://hub.docker.com/_/ruby/) have `-slim` tag variants
+designed to fill the need for optimization. Even when these "slim" variants are
+insufficient, it is still recommended to inherit from an Official Repository
+base OS image to leverage the ongoing maintenance work, rather than duplicating
+these efforts.
+
+## How do I know the Official Repositories are secure?
+
+Docker provides a preview version of Docker Cloud's [Security Scanning service](http://docs.docker.com/docker-cloud/builds/image-scan/) for all of the
+Official Repositories located on Docker Hub. These security scan results provide
+valuable information about which images contain security vulnerabilities, which
+you should use to help you choose secure components for your own projects.
+
+To view the Docker Security Scanning results:
+
+1. Make sure you're logged in to Docker Hub.
+ You can view Official Images even while logged out, however the scan results are only available once you log in.
+2. Navigate to the official repository whose security scan you want to view.
+3. Click the `Tags` tab to see a list of tags and their security scan summaries.
+ 
+
+You can click into a tag's detail page to see more information about which
+layers in the image and which components within the layer are vulnerable.
+Details including a link to the official CVE report for the vulnerability appear
+when you click an individual vulnerable component.
+
+## How can I get involved?
+
+All Official Repositories contain a **User Feedback** section in their
+documentation which covers the details for that specific repository. In most
+cases, the GitHub repository which contains the Dockerfiles for an Official
+Repository also has an active issue tracker. General feedback and support
+questions should be directed to `#docker-library` on Freenode IRC.
+
+## How do I create a new Official Repository?
+
+From a high level, an Official Repository starts out as a proposal in the form
+of a set of GitHub pull requests. You'll find detailed and objective proposal
+requirements in the following GitHub repositories:
+
+* [docker-library/official-images](https://github.com/docker-library/official-images)
+
+* [docker-library/docs](https://github.com/docker-library/docs)
+
+The Official Repositories team, with help from community contributors, formally
+review each proposal and provide feedback to the author. This initial review
+process may require a bit of back and forth before the proposal is accepted.
+
+There are also subjective considerations during the review process. These
+subjective concerns boil down to the basic question: "is this image generally
+useful?" For example, the [python](https://hub.docker.com/_/python/)
+Official Repository is "generally useful" to the large Python developer
+community, whereas an obscure text adventure game written in Python last week is
+not.
+
+Once a new proposal is accepted, the author is responsibile for keeping
+their images up-to-date and responding to user feedback. The Official
+Repositories team becomes responsibile for publishing the images and
+documentation on Docker Hub. Updates to the Official Repository follow the same
+pull request process, though with less review. The Official Repositories team
+ultimately acts as a gatekeeper for all changes, which helps mitigate the risk
+of quality and security issues from being introduced.
diff --git a/docs/orgs.md b/docs/orgs.md
new file mode 100644
index 0000000000..d14f5a1dce
--- /dev/null
+++ b/docs/orgs.md
@@ -0,0 +1,53 @@
++++
+title = "Teams & Organizations"
+description = "Docker Hub Teams and Organizations"
+keywords = ["Docker, docker, registry, teams, organizations, plans, Dockerfile, Docker Hub, docs, documentation"]
+[menu.main]
+parent="mn_pubhub"
+weight=-80
++++
+
+# Organizations and teams
+
+Docker Hub [organizations](https://hub.docker.com/organizations/) let you
+create teams so you can give colleagues access to shared image repositories.
+A Docker Hub organization can contain public and private repositories just like
+a user account.
+Access to push or pull for these repositories is allocated by defining teams of users and then assigning team rights to specific repositories. Repository
+creation is limited to users in the organization owner's group. This allows you
+to distribute limited access Docker images, and to select which Docker Hub users
+can publish new images.
+
+### Creating and viewing organizations
+
+You can see which organizations you belong to and add new organizations by clicking "Organizations" in the top nav bar.
+
+
+
+### Organization teams
+
+Users in the "Owners" team of an organization can create and modify the
+membership of all teams.
+
+Other users can only see teams they belong to.
+
+
+
+### Repository team permissions
+
+Use teams to manage who can interact with your repositories.
+
+You need to be a member of the organization's "Owners" team to create a new team,
+Hub repository, or automated build. As an "Owner", you then delegate the following
+repository access rights to a team using the "Collaborators" section of the repository view:
+
+- `Read` access allows a user to view, search, and pull a private repository in the same way as they can a public repository.
+- `Write` access users are able to push to non-automated repositories on the Docker Hub.
+- `Admin` access allows the user to modify the repositories "Description", "Collaborators" rights,
+ "Public/Private" visibility and "Delete".
+
+> **Note**: A User who has not yet verified their email address will only have
+> `Read` access to the repository, regardless of the rights their team
+> membership has given them.
+
+
diff --git a/docs/repos.md b/docs/repos.md
new file mode 100644
index 0000000000..7f47b83cf1
--- /dev/null
+++ b/docs/repos.md
@@ -0,0 +1,270 @@
++++
+title = "Repositories on Docker Hub"
+description = "Your Repositories on Docker Hub"
+keywords = ["Docker, docker, trusted, registry, accounts, plans, Dockerfile, Docker Hub, webhooks, docs, documentation"]
+[menu.main]
+parent="mn_pubhub"
+weight=5
++++
+
+# Your Hub repositories
+
+Docker Hub repositories let you share images with co-workers,
+customers, or the Docker community at large. If you're building your images internally,
+either on your own Docker daemon, or using your own Continuous integration services,
+you can push them to a Docker Hub repository that you add to your Docker Hub user or
+organization account.
+
+Alternatively, if the source code for your Docker image is on GitHub or Bitbucket,
+you can use an "Automated build" repository, which is built by the Docker Hub
+services. See the [automated builds documentation](builds.md) to read about
+the extra functionality provided by those services.
+
+
+
+## Searching for images
+
+You can search the [Docker Hub](https://hub.docker.com) registry via its search
+interface or by using the command line interface. Searching can find images by image
+name, user name, or description:
+
+ $ docker search centos
+ NAME DESCRIPTION STARS OFFICIAL AUTOMATED
+ centos The official build of CentOS. 1034 [OK]
+ ansible/centos7-ansible Ansible on Centos7 43 [OK]
+ tutum/centos Centos image with SSH access. For the root... 13 [OK]
+ ...
+
+There you can see two example results: `centos` and `ansible/centos7-ansible`. The second
+result shows that it comes from the public repository of a user, named
+`ansible/`, while the first result, `centos`, doesn't explicitly list a
+repository which means that it comes from the top-level namespace for
+[Official Repositories](official_repos.md). The `/` character separates
+a user's repository from the image name.
+
+Once you've found the image you want, you can download it with `docker pull `:
+
+ $ docker pull centos
+ latest: Pulling from centos
+ 6941bfcbbfca: Pull complete
+ 41459f052977: Pull complete
+ fd44297e2ddb: Already exists
+ centos:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.
+ Digest: sha256:d601d3b928eb2954653c59e65862aabb31edefa868bd5148a41fa45004c12288
+ Status: Downloaded newer image for centos:latest
+
+You now have an image from which you can run containers.
+
+## Viewing repository tags
+
+Docker Hub's repository "Tags" view shows you the available tags and the size
+of the associated image.
+
+Image sizes are the cumulative space taken up by the image and all
+its parent images. This is also the disk space used by the contents of the
+Tar file created when you `docker save` an image.
+
+
+
+## Creating a new repository on Docker Hub
+
+When you first create a Docker Hub user, you will have a "Get started with Docker Hub."
+screen, from which you can click directly into "Create Repository".
+You can also use the "Create ▼" menu to "Create Repository".
+
+When creating a new repository, you can choose to put it in your Docker ID namespace, or that of any [organization](orgs.md) that you
+are in the "Owners" team.
+The Repository Name will need to be unique in that namespace, can be two to 255 characters,
+and can only contain lowercase letters, numbers or `-` and `_`.
+
+The "Short Description" of 100 characters will be used in the search results, while the
+"Full Description" can be used as the Readme for the repository, and can use Markdown to
+add simple formatting.
+
+After you hit the "Create" button, you then need to `docker push` images to that Hub based
+repository.
+
+
+
+## Pushing a repository image to Docker Hub
+
+In order to push a repository to the Docker Hub, you need to
+name your local image using your Docker Hub username, and the
+repository name that you created in the previous step.
+You can add multiple images to a repository, by adding a specific `:` to
+it (for example `docs/base:testing`). If its not specified, the tag defaults to
+`latest`.
+You can name your local images either when you build it, using
+`docker build -t /[:]`,
+by re-tagging an existing local image `docker tag /[:]`,
+or by using `docker commit /[:]` to commit
+changes.
+See [Working with Docker images](https://docs.docker.com/userguide/dockerimages) for a detailed description.
+
+Now you can push this repository to the registry designated by its name or tag.
+
+ $ docker push /:
+
+The image will then be uploaded and available for use by your team-mates and/or the
+community.
+
+
+## Stars
+
+Your repositories can be starred and you can star repositories in
+return. Stars are a way to show that you like a repository. They are
+also an easy way of bookmarking your favorites.
+
+## Comments
+
+You can interact with other members of the Docker community and maintainers by
+leaving comments on repositories. If you find any comments that are not
+appropriate, you can flag them for review.
+
+## Collaborators and their role
+
+A collaborator is someone you want to give access to a private
+repository. Once designated, they can `push` and `pull` to your
+repositories. They will not be allowed to perform any administrative
+tasks such as deleting the repository or changing its status from
+private to public.
+
+> **Note:**
+> A collaborator cannot add other collaborators. Only the owner of
+> the repository has administrative access.
+
+You can also assign more granular collaborator rights ("Read", "Write", or "Admin")
+on Docker Hub by using organizations and teams. For more information
+see the [organizations documentation](orgs.md).
+
+## Private repositories
+
+Private repositories allow you to have repositories that contain images
+that you want to keep private, either to your own account or within an
+organization or team.
+
+To work with a private repository on [Docker
+Hub](https://hub.docker.com), you will need to add one via the [Add
+Repository](https://hub.docker.com/add/repository/)
+button. You get one private repository for free with your Docker Hub
+user account (not usable for organizations you're a member of). If
+you need more accounts you can upgrade your [Docker
+Hub](https://hub.docker.com/account/billing-plans/) plan.
+
+Once the private repository is created, you can `push` and `pull` images
+to and from it using Docker.
+
+> *Note:* You need to be signed in and have access to work with a
+> private repository.
+
+Private repositories are just like public ones. However, it isn't
+possible to browse them or search their content on the public registry.
+They do not get cached the same way as a public repository either.
+
+It is possible to give access to a private repository to those whom you
+designate (i.e., collaborators) from its "Settings" page. From there, you
+can also switch repository status (*public* to *private*, or
+vice-versa). You will need to have an available private repository slot
+open before you can do such a switch. If you don't have any available,
+you can always upgrade your [Docker
+Hub](https://hub.docker.com/account/billing-plans/) plan.
+
+## Webhooks
+
+A webhook is an HTTP call-back triggered by a specific event.
+You can use a Hub repository webhook to notify people, services, and other
+applications after a new image is pushed to your repository (this also happens
+for Automated builds). For example, you can trigger an automated test or
+deployment to happen as soon as the image is available.
+
+To get started adding webhooks, go to the desired repository in the Hub,
+and click "Webhooks" under the "Settings" box.
+A webhook is called only after a successful `push` is
+made. The webhook calls are HTTP POST requests with a JSON payload
+similar to the example shown below.
+
+*Example webhook JSON payload:*
+
+```json
+{
+ "callback_url": "https://registry.hub.docker.com/u/svendowideit/busybox/hook/2141bc0cdec4hebec411i4c1g40242eg110020/",
+ "push_data": {
+ "images": [
+ "27d47432a69bca5f2700e4dff7de0388ed65f9d3fb1ec645e2bc24c223dc1cc3",
+ "51a9c7c1f8bb2fa19bcd09789a34e63f35abb80044bc10196e304f6634cc582c",
+ "..."
+ ],
+ "pushed_at": 1.417566822e+09,
+ "pusher": "svendowideit"
+ },
+ "repository": {
+ "comment_count": 0,
+ "date_created": 1.417566665e+09,
+ "description": "",
+ "full_description": "webhook triggered from a 'docker push'",
+ "is_official": false,
+ "is_private": false,
+ "is_trusted": false,
+ "name": "busybox",
+ "namespace": "svendowideit",
+ "owner": "svendowideit",
+ "repo_name": "svendowideit/busybox",
+ "repo_url": "https://registry.hub.docker.com/u/svendowideit/busybox/",
+ "star_count": 0,
+ "status": "Active"
+ }
+}
+```
+
+
+
+>**Note:** If you want to test your webhook, we recommend using a tool like
+>[requestb.in](http://requestb.in/). Also note, the Docker Hub server can't be
+>filtered by IP address.
+
+### Webhook chains
+
+Webhook chains allow you to chain calls to multiple services. For example,
+you can use this to trigger a deployment of your container only after
+it has been successfully tested, then update a separate Changelog once the
+deployment is complete.
+After clicking the "Add webhook" button, simply add as many URLs as necessary
+in your chain.
+
+The first webhook in a chain will be called after a successful push. Subsequent
+URLs will be contacted after the callback has been validated.
+
+### Validating a callback
+
+In order to validate a callback in a webhook chain, you need to
+
+1. Retrieve the `callback_url` value in the request's JSON payload.
+1. Send a POST request to this URL containing a valid JSON body.
+
+> **Note**: A chain request will only be considered complete once the last
+> callback has been validated.
+
+To help you debug or simply view the results of your webhook(s),
+view the "History" of the webhook available on its settings page.
+
+#### Callback JSON data
+
+The following parameters are recognized in callback data:
+
+* `state` (required): Accepted values are `success`, `failure` and `error`.
+ If the state isn't `success`, the webhook chain will be interrupted.
+* `description`: A string containing miscellaneous information that will be
+ available on the Docker Hub. Maximum 255 characters.
+* `context`: A string containing the context of the operation. Can be retrieved
+ from the Docker Hub. Maximum 100 characters.
+* `target_url`: The URL where the results of the operation can be found. Can be
+ retrieved on the Docker Hub.
+
+*Example callback payload:*
+
+ {
+ "state": "success",
+ "description": "387 tests PASSED",
+ "context": "Continuous integration by Acme CI",
+ "target_url": "http://ci.acme.com/results/afd339c1c3d27"
+ }
diff --git a/docs/s3_website.json b/docs/s3_website.json
new file mode 100644
index 0000000000..96eea7318e
--- /dev/null
+++ b/docs/s3_website.json
@@ -0,0 +1,8 @@
+{
+ "ErrorDocument": {
+ "Key": "jsearch/index.html"
+ },
+ "IndexDocument": {
+ "Suffix": "index.html"
+ }
+}
diff --git a/docs/webhooks.md b/docs/webhooks.md
new file mode 100644
index 0000000000..a50206e3f6
--- /dev/null
+++ b/docs/webhooks.md
@@ -0,0 +1,50 @@
++++
+title = "Webhooks for automated builds"
+description = "Docker Hub Automated Builds"
+keywords = ["Docker, webhookds, hub, builds"]
+[menu.main]
+parent="mn_pubhub"
+weight=7
++++
+
+# Webhooks for automated builds
+
+If you have an automated build repository in Docker Hub, you can use Webhooks to cause an action in another application in response to an event in the repository. Docker Hub webhooks fire when an image is built in, or a new tag added to, your automated build repository.
+
+With your webhook, you specify a target URL and a JSON payload to deliver. The example webhook below generates an HTTP POST that delivers a JSON payload:
+
+```json
+{
+ "callback_url": "https://registry.hub.docker.com/u/svendowideit/testhook/hook/2141b5bi5i5b02bec211i4eeih0242eg11000a/",
+ "push_data": {
+ "images": [
+ "27d47432a69bca5f2700e4dff7de0388ed65f9d3fb1ec645e2bc24c223dc1cc3",
+ "51a9c7c1f8bb2fa19bcd09789a34e63f35abb80044bc10196e304f6634cc582c",
+ "..."
+ ],
+ "pushed_at": 1.417566161e+09,
+ "pusher": "trustedbuilder"
+ },
+ "repository": {
+ "comment_count": "0",
+ "date_created": 1.417494799e+09,
+ "description": "",
+ "dockerfile": "#\n# BUILD\u0009\u0009docker build -t svendowideit/apt-cacher .\n# RUN\u0009\u0009docker run -d -p 3142:3142 -name apt-cacher-run apt-cacher\n#\n# and then you can run containers with:\n# \u0009\u0009docker run -t -i -rm -e http_proxy http://192.168.1.2:3142/ debian bash\n#\nFROM\u0009\u0009ubuntu\nMAINTAINER\u0009SvenDowideit@home.org.au\n\n\nVOLUME\u0009\u0009[\/var/cache/apt-cacher-ng\]\nRUN\u0009\u0009apt-get update ; apt-get install -yq apt-cacher-ng\n\nEXPOSE \u0009\u00093142\nCMD\u0009\u0009chmod 777 /var/cache/apt-cacher-ng ; /etc/init.d/apt-cacher-ng start ; tail -f /var/log/apt-cacher-ng/*\n,
+ full_description: Docker Hub based automated build from a GitHub repo",
+ "is_official": false,
+ "is_private": true,
+ "is_trusted": true,
+ "name": "testhook",
+ "namespace": "svendowideit",
+ "owner": "svendowideit",
+ "repo_name": "svendowideit/testhook",
+ "repo_url": "https://registry.hub.docker.com/u/svendowideit/testhook/",
+ "star_count": 0,
+ "status": "Active"
+ }
+}
+```
+
+>**Note:** If you want to test your webhook, we recommend using a tool like
+>[requestb.in](http://requestb.in/). Also note, the Docker Hub server can't be
+>filtered by IP address.
diff --git a/fabfile.py b/fabfile.py
new file mode 100644
index 0000000000..80d2a1aedc
--- /dev/null
+++ b/fabfile.py
@@ -0,0 +1,14 @@
+from fabric.api import run
+
+def start_project(email="none", user="none", auth="none", beta_password="maybejustnotsomeemptyspaceyea?", sha="latest", new_relic_key="", new_relic_app_name="hub-stage-node"):
+ run('docker rm $(docker ps -a -q) > /dev/null 2>&1 || :')
+ run('docker rmi $(docker images -q) > /dev/null 2>&1 || :')
+ run('cd /home/')
+ run('docker login -e %s -u %s -p %s' % (email, user, auth))
+ run('docker pull bagel/hub-prod:%s' % sha)
+ run('docker pull bagel/haproxy_beta:latest')
+ run("docker ps | awk '{if($1 != \"CONTAINER\"){print $1}}' | xargs -r docker kill")
+ # We should tag the image with the git commit and deploy that instead of "latest"
+ run('docker run -dp 7001:3000 -e ENV=production --restart=on-failure:5 -e HUB_API_BASE_URL=https://hub-beta-stage.docker.com -e REGISTRY_API_BASE_URL=https://hub-beta-stage.docker.com -e NEW_RELIC_LICENSE_KEY=%s -e NEW_RELIC_APP_NAME=%s bagel/hub-prod:%s' % (new_relic_key, new_relic_app_name, sha))
+ # HAProxy doesn't change a lot. We should check the image names before killing/rebooting
+ run('docker run -dp 80:80 -p 443:443 -e BETA_PASSWORD=%s --restart=on-failure:5 -v /opt/haproxy.pem:/haproxy/keys/hub-beta.docker.com/hub-beta.docker.pem bagel/haproxy_beta:latest' % beta_password)
diff --git a/flow-libs/async.js b/flow-libs/async.js
new file mode 100644
index 0000000000..2d9f0cccef
--- /dev/null
+++ b/flow-libs/async.js
@@ -0,0 +1,12 @@
+type AsyncCallback = (err: ?Object, results: ?any) => void;
+type ParallelFuncs = (callback: AsyncCallback) => void;
+
+declare module 'async' {
+ declare function parallel(tasks: Array | Object,
+ callback: AsyncCallback): void
+ declare function series(tasks: Array,
+ callback: AsyncCallback): void
+ declare function each(arr: Array,
+ func: Function,
+ callback: Function): void
+}
\ No newline at end of file
diff --git a/flow-libs/debug.js b/flow-libs/debug.js
new file mode 100644
index 0000000000..4807c40f8a
--- /dev/null
+++ b/flow-libs/debug.js
@@ -0,0 +1,5 @@
+type DebugFunction = (thing: any) => void;
+
+declare module 'debug' {
+ declare function exports(string: string): DebugFunction;
+}
\ No newline at end of file
diff --git a/flow-libs/fluxible.js b/flow-libs/fluxible.js
new file mode 100644
index 0000000000..3ad2f4e95f
--- /dev/null
+++ b/flow-libs/fluxible.js
@@ -0,0 +1,4 @@
+export type FluxibleActionContext = {
+ dispatch(eventName: string,
+ payload: any): void;
+}
diff --git a/flow-libs/hub-js-sdk.js b/flow-libs/hub-js-sdk.js
new file mode 100644
index 0000000000..79da45ff99
--- /dev/null
+++ b/flow-libs/hub-js-sdk.js
@@ -0,0 +1,74 @@
+type SuperAgentCallback = (err: any,
+ res: any) => void;
+
+type JWT = String;
+type ChangePasswordData = {
+ username: String;
+ oldpassword: String;
+ newpassword: String
+}
+
+declare module 'hub-js-sdk' {
+ declare var Auth: {
+ getToken(username: string,
+ password: string,
+ cb: SuperAgentCallback): void;
+ }
+ declare var Repositories: {
+ createRepository(jwt: JWT,
+ repository: any,
+ cb: SuperAgentCallback): void;
+ getReposForUser(jwt: JWT,
+ username: String,
+ cb: SuperAgentCallback): void
+ }
+ declare var Emails: {
+ getEmailSubscriptions(JWT: JWT,
+ user: String,
+ cb: SuperAgentCallback): void;
+ unsubscribeEmails(JWT: JWT,
+ user: String,
+ data: Object,
+ cb: SuperAgentCallback): void;
+ subscribeEmails(JWT:JWT,
+ user: String,
+ data: Object,
+ cb: SuperAgentCallback): void;
+ getEmailsJWT(JWT:JWT,
+ cb:SuperAgentCallback): void;
+ getEmailsForUser(JWT: JWT,
+ user: String,
+ cb: SuperAgentCallback): void;
+ deleteEmailByID(JWT: JWT,
+ id: String,
+ cb: SuperAgentCallback): void;
+ updateEmailByID(JWT: JWT,
+ id: String,
+ data: Object,
+ cb: SuperAgentCallback): void;
+ addEmailsForUser(JWT: JWT,
+ user: Object,
+ email: string,
+ cb: SuperAgentCallback): void;
+ }
+}
+
+declare module 'hub-js-sdk/src/Hub/SDK/Users' {
+ declare function changePassword(JWT: JWT,
+ data: ChangePasswordData,
+ cb: SuperAgentCallback): void;
+ declare function getUser(JWT: JWT,
+ user: String,
+ cb: SuperAgentCallback): void;
+}
+
+declare module 'hub-js-sdk/src/Hub/SDK/Auth' {
+ declare function getToken(username: String,
+ password: String,
+ cb: SuperAgentCallback): void;
+}
+
+declare module 'hub-js-sdk/src/Hub/SDK/Notifications' {
+ declare function getActivityFeed(JWT: JWT,
+ cb: SuperAgentCallback): void;
+}
diff --git a/flow-libs/lodash.js b/flow-libs/lodash.js
new file mode 100644
index 0000000000..a047b9e1b5
--- /dev/null
+++ b/flow-libs/lodash.js
@@ -0,0 +1,5 @@
+declare module 'lodash' {
+ declare function sortByOrder(arr: Array,
+ properties: Array,
+ sortOrder: Array): Array;
+}
\ No newline at end of file
diff --git a/gulp-tasks/img.js b/gulp-tasks/img.js
new file mode 100644
index 0000000000..524d9a86f6
--- /dev/null
+++ b/gulp-tasks/img.js
@@ -0,0 +1,24 @@
+var gulp = require('gulp');
+var imagemin = require('gulp-imagemin');
+var pngquant = require('imagemin-pngquant');
+
+//Hub2 Images for dev & production (There is a separate task for docker-ux images)
+gulp.task('images::dev', function () {
+ return gulp.src('app/img/**')
+ .pipe(imagemin({
+ progressive: true,
+ svgoPlugins: [{removeViewBox: false}],
+ use: [pngquant({ quality: '65-80', speed: 4 })]
+ }))
+ .pipe(gulp.dest('app/.build/public/img'));
+});
+
+gulp.task('images::prod', function() {
+ return gulp.src('app/img/**')
+ .pipe(imagemin({
+ progressive: true,
+ svgoPlugins: [{removeViewBox: false}],
+ use: [pngquant({ quality: '65-80', speed: 4 })]
+ }))
+ .pipe(gulp.dest('.tmp/server/build/img'));
+});
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000000..05734a32b9
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,3 @@
+'use strict';
+require('./gulp-tasks/img');
+
diff --git a/local.Dockerfile b/local.Dockerfile
new file mode 100644
index 0000000000..34163d2fd2
--- /dev/null
+++ b/local.Dockerfile
@@ -0,0 +1,22 @@
+FROM bagel/universe:337f873f4f23f4b2603972229ae3519c5f61f6d7
+
+ENV ENV local
+ENV NODE_ENV local
+
+COPY ./app /opt/hub/app
+COPY ./Makefile /opt/hub/Makefile
+COPY ./_webpack /opt/hub/_webpack
+COPY ./gulpfile.js /opt/hub/gulpfile.js
+COPY ./gulp-tasks /opt/hub/gulp-tasks
+COPY ./app-server /opt/hub/app-server
+COPY ./.eslintrc /opt/hub/.eslintrc
+
+RUN make server-prod-target
+RUN make server-extras
+RUN make js-local
+RUN make images-prod
+RUN make docker-font-prod
+RUN gulp images::prod
+RUN make styles-base-prod
+RUN make stats-dir
+RUN make css-stats
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..fe9abf952a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,114 @@
+{
+ "name": "docker-2.0",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "test": "jest",
+ "build:dev": "DEBUG=* webpack -dw"
+ },
+ "jest": {
+ "rootDir": "./app/scripts",
+ "scriptPreprocessor": "../../node_modules/babel-jest",
+ "testFileExtensions": [
+ "js"
+ ],
+ "moduleFileExtensions": [
+ "jsx",
+ "js",
+ "json"
+ ],
+ "modulePathIgnorePatterns": [
+ "/node_modules/"
+ ],
+ "unmockedModulePathPatterns": [
+ "react"
+ ]
+ },
+ "dependencies": {
+ "@dux/element-button": "0.0.3",
+ "@dux/element-card": "0.0.7",
+ "@dux/element-markdown": "0.0.8",
+ "@dux/hub-sdk": "^0.1.1",
+ "async": "^1.3.0",
+ "babel": "^5.6.14",
+ "babel-core": "^5.6.14",
+ "babel-runtime": "^5.6.18",
+ "body-parser": "^1.12.2",
+ "bugsnag": "^1.7.0",
+ "classnames": "^2.1.2",
+ "cookie": "^0.2.3",
+ "cookie-parser": "^1.3.4",
+ "csurf": "^1.8.0",
+ "debug": "^2.1.3",
+ "dux": "file:./private-deps/docker-ux",
+ "express": "^4.12.3",
+ "express-state": "^1.2.0",
+ "fluxible": "^1.0.3",
+ "fluxible-addons-react": "^0.2.0",
+ "highlight.js": "^9.0.0",
+ "history": "^1.17.0",
+ "hub-js-sdk": "file:./private-deps/hub-js-sdk",
+ "immutable": "^3.7.6",
+ "keymirror": "^0.1.1",
+ "lodash": "^3.6.0",
+ "marked": "^0.3.3",
+ "md5": "^2.0.0",
+ "moment": "^2.10.3",
+ "newrelic": "christopherbiscardi/node-newrelic#c4ccca3764acafaf9c5899e4a1abece828e1f7b8",
+ "normalizr": "^1.4.0",
+ "numeral": "^1.5.3",
+ "rc-tooltip": "^3.3.0",
+ "react": "^0.14.7",
+ "react-document-title": "^2.0.2",
+ "react-dom": "^0.14.3",
+ "react-router": "^1.0.0",
+ "react-select": "^1.0.0-beta6",
+ "recurly-js": "git://github.com/recurly/recurly-js#d9740eb3ee416fb999635daecfb524a492dbb058",
+ "redux": "^3.0.5",
+ "redux-logger": "^2.3.2",
+ "redux-ui": "0.0.8",
+ "remarkable": "^1.6.0",
+ "reselect": "^2.0.1",
+ "serialize-javascript": "^1.0.0",
+ "serve-favicon": "^2.2.0",
+ "superagent": "^1.1.0",
+ "svg-inline-react": "^0.3.1",
+ "velocity-animate": "^1.2.3",
+ "velocity-react": "1.1.3"
+ },
+ "devDependencies": {
+ "babel-eslint": "^4.0.0",
+ "babel-jest": "^5.0.1",
+ "babel-loader": "^5.0.0",
+ "css-loader": "^0.23.0",
+ "cssnano": "^3.2.0",
+ "cssstats": "^1.10.0",
+ "eslint": "^1.2.1",
+ "eslint-loader": "^1.0.0",
+ "extract-text-webpack-plugin": "^0.9.1",
+ "gulp": "^3.8.11",
+ "gulp-imagemin": "^2.2.1",
+ "imagemin-pngquant": "^4.0.0",
+ "json-loader": "^0.5.2",
+ "lost": "^6.6.2",
+ "nodemon": "^1.3.7",
+ "postcss-browser-reporter": "^0.4.0",
+ "postcss-constants": "^0.1.1",
+ "postcss-cssnext": "^2.1.0",
+ "postcss-cssstats": "^1.0.0",
+ "postcss-each": "^0.7.0",
+ "postcss-import": "^7.0.0",
+ "postcss-loader": "^0.8.0",
+ "postcss-nested": "^1.0.0",
+ "postcss-url": "^5.0.1",
+ "react-redux": "^4.0.3",
+ "redux": "^3.0.5",
+ "reselect": "^2.0.1",
+ "style-loader": "^0.13.0",
+ "svg-inline-loader": "^0.4.0",
+ "webpack": "^1.10"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+}
diff --git a/pr-docs/css.md b/pr-docs/css.md
new file mode 100644
index 0000000000..f019c08e6c
--- /dev/null
+++ b/pr-docs/css.md
@@ -0,0 +1,62 @@
+[Demo](https://css-modules.github.io/webpack-demo/)
+
+The approach is thus: Use Foundation as a "browser reset" stylesheet,
+then put everything that isn't a foundation `_settings.scss` variable
+in CSSModule sidecar files. This links our javascript modules with our
+css and increases the ease with which we can create a module library.
+
+# Implementation
+
+## File Structure
+
+```
+app/scripts/
+|-- ScopedSelectors.js
+|-- ScopedSelectors.css
+```
+
+## Usage
+
+```javascript
+import styles from './ScopedSelectors.css';
+
+import React, { Component } from 'react';
+
+export default class ScopedSelectors extends Component {
+
+ render() {
+ return (
+
+
Scoped Selectors
+
+ );
+ }
+
+};
+```
+
+```css
+.root {
+ border-width: 2px;
+ border-style: solid;
+ border-color: #777;
+ padding: 0 20px;
+ margin: 0 6px;
+ max-width: 400px;
+}
+
+.text {
+ color: #777;
+ font-size: 24px;
+ font-family: helvetica, arial, sans-serif;
+ font-weight: 600;
+}
+```
+
+# Approach
+
+* modules should be scoped to themselves and not affect children or
+ siblings.
+* Webpack already has support for css-modules in it's `css-loader`. So
+ we'll start with that.
+* [css-modules and preprocessors (sass)](https://github.com/css-modules/css-modules#usage-with-preprocessors)
diff --git a/pr-docs/linting.md b/pr-docs/linting.md
new file mode 100644
index 0000000000..e66ad8c472
--- /dev/null
+++ b/pr-docs/linting.md
@@ -0,0 +1,28 @@
+# Linting
+
+All code must pass [ESLint][eslint] before being merged into master
+branch. The ESLint config can be found in `.eslintrc` and is
+integrated into webpack.
+
+# Running ESLint
+
+```
+gulp webpack
+```
+
+Since linting is integrated with webpack, it is possible to lint code
+while it is being developed without any extra effort. This is
+important because if it is not approximate to effortless to run
+linting, it will not be run while developing.
+
+# We should block deploys for linting errors
+
+Since we do CI/CD, the static analysis present in ESLint can help us
+catch bugs before shipping. We should therefore block deploys if
+ESLint detects an error-level (level `2` in `.eslintrc`) issue.
+
+* [eslint][eslint]
+* [babel-eslint][babel-eslint]
+
+[eslint]: http://eslint.org/
+[babel-eslint]: https://github.com/babel/babel-eslint
diff --git a/pr-docs/routes.md b/pr-docs/routes.md
new file mode 100644
index 0000000000..99876f2e52
--- /dev/null
+++ b/pr-docs/routes.md
@@ -0,0 +1,262 @@
+# Routes
+
+Two items affect this proposal.
+
+1. [Distribution's work](https://github.com/docker/distribution),
+ specifically relating to defining Repositories, Images, Manifests,
+ Digests and Tags.
+2. The Current Hub's Routing Issues
+
+## Distributions Work (partially summarized)
+
+### Repository
+
+* A set of blobs
+* Subsets of these blobs make up Images
+
+### Image
+
+* A set of blobs
+ - Layers
+ - Tag
+ - Signatures
+ - Manifest
+* A Tag (potentially containing signatures) points to a Manifest
+* A Manifest points to multiple layers.
+
+### Manifest
+
+As defined in the [distribution][manifest-pr] Manifest PR:
+
+> A [Content Manifest][manifest] is a simple JSON file which contains
+> general fields that are typical to any package management
+> system. The goal is for these manifests to describe an application
+> and its dependencies in a content-addressable and verifiable way.
+
+### Tag
+
+As defined in the [distribution][d-tag-pr] PR:
+
+> A [tag][tag] is simply a named pointer to content. The content can
+> be any blob but should mostly be a manifest. One can sign tags to
+> later verify that they were created by a trusted party.
+
+### Additional Content
+
+Image names will be allowed to have many slashes in the future.
+
+## Current Hub Issues
+
+### Collisions
+
+The URLs for user and repo collide:
+
+A user's Starred Repos:
+
+```
+/u/biscarch/starred/
+```
+
+A user's repository, named Starred.
+
+```
+/u/biscarch/starred/
+```
+
+## Future Problems
+
+An image for the user `biscarch`, named `my/repo`:
+
+```
+/u/biscarch/my/repo/
+```
+
+An image for the user `biscarch`, named `my`, tagged `repo`:
+
+```
+/u/biscarch/my/repo/
+```
+
+## Solutions
+
+Namespace `Users`, `Repos` and `Images` as such (with the user
+`biscarch`)
+
+```
+/u/:user
+/r/:user/:repo
+/i/:user/:repo/:tag
+```
+
+### Solving Starred Repos
+
+Prefix defines whether we are referring to a repo or attribute of a
+user:
+
+```
+/u/biscarch/starred
+/r/biscarch/starred
+```
+
+### Solving Repo/Image Conflicts
+
+Prefix determines whether we are referring to a Repository or Image:
+
+```
+/r/biscarch/my/repo/
+/i/biscarch/my/repo/
+```
+
+## The new Spec
+
+```
+/u/
+/u/:user/
+/r/:user/:repo/
+/i/:user/:repo/:tag/
+```
+
+### Full List
+
+### Dashboard
+
+`/`
+
+### Official Repositories
+
+"username" === library, which is represented as the root `_`.
+All management of `library` namespaced repos is done from the usual
+`/u/library/:repo/`
+
+```
+/_/:repo/
+/_/:repo/dockerfile/
+/_/:repo/dockerfile/raw
+/_/:repo/tags/
+```
+
+### Single Endpoints
+
+* Search
+ - `/search/`
+* Plans
+ - `/plans/`
+
+### Account
+
+Mostly Settings; Add Repository Page;
+
+`/account/` should redirect to `/account/settings/`
+
+```
+/account/accounts/
+/account/authorized_services/
+/account/change-password/
+/account/confirm-email//
+/account/emails/
+/account/notifications/
+/account/organizations/
+/account/organizations/:org_name/
+/account/organizations/:org_name/groups/:group_id/
+/account/repositories/add/
+/account/settings/
+/account/subscriptions/
+```
+
+### Users
+
+```
+/u/
+/u/:user/
+/u/:user/activity/
+/u/:user/contributed/
+/u/:user/starred/
+```
+
+### Repos
+
+```
+/r/:user/:repo/
+/r/:user/:repo/~/settings/
+/r/:user/:repo/~/settings/collaborators/
+/r/:user/:repo/~/settings/links/
+/r/:user/:repo/~/settings/triggers/
+/r/:user/:repo/~/settings/webhooks/
+/r/:user/:repo/~/settings/tags/
+```
+
+Current build history urls:
+
+```
+/r/:user/:repo/~/builds_history/
+```
+
+### Images
+
+We currently don't do a lot for Images. Repositories have been the
+main focus.
+
+```
+/i/:user/:repo/:tag/
+/i/:user/:repo/:tag/~/dockerfile/
+/i/:user/:repo/:tag/~/dockerfile/raw/
+```
+
+### Automated Builds
+
+```
+/automated-builds/
+/builds/
+/builds/:user/:repo/
+```
+
+### Convenience Redirects
+
+Also, potential pages to build out more agressively.
+
+* `/official/`
+ - redirects to `/search?q=library&f=official`
+ - future: Potentially `Explore` type page for official repos
+* `/most_stars/`, `/popular/`
+ - redirects to `search?q=library&s=stars`
+* `/recent_updated/`
+ - `search?q=library&s=last_updated`
+
+#### Help
+
+* `/help`
+ - `https://www.docker.com/resources/help/`
+ - Can we rely on this url to stick around?
+* `/help/docs`
+ - `https://docs.docker.com/`
+
+## Make Separate Sites for:
+
+### Highland URLs
+
+We need to pull out the APIs used on the current Hub for this.
+
+```
+/highland/
+/highland/build-configs/
+/highland/builds/
+/highland/search/
+/highland/stats/
+```
+
+
+## More Issues
+
+* There are no links to comments
+* `/opensearch.xml` times out on the current site
+ - Should we re-implement?
+* `/sitemap.xml`
+
+# Concerns with this Proposal
+
+* Automated Build urls need to be given more thought
+
+[tag]: https://github.com/stevvooe/distribution/blob/a8d3f3474b7b60576dc64250d95db3717bf07c33/doc/spec/tags.md#tags
+[d-tag-pr]: https://github.com/docker/distribution/pull/173/files
+[d-manifest-pr]: https://github.com/docker/distribution/pull/62
+[manifest]: https://github.com/jlhawn/distribution/blob/e8b5c8c32b565b9b643c3a0b0e87339bf40eb206/doc/spec/manifest.md
diff --git a/production_ready.md b/production_ready.md
new file mode 100644
index 0000000000..ee92e5a3b3
--- /dev/null
+++ b/production_ready.md
@@ -0,0 +1,93 @@
+Production Readiness: Docker Hub Front-End (hub-web-v2)
+================================
+
+Testing
+-------
+
+ * **What is the max traffic load that your service has been tested with?**
+ Hub UI has not been load tested.
+
+ * **How has the service been soak-tested?**
+ Hub UI has not been soak tested.
+
+ Monitoring
+ ----------
+
+ * **How do you monitor?**
+ New Relic for server monitoring, BugSnag for JavaScript errors and PagerDuty for alerting.
+
+ * **What’s the link(s) to the dashboard(s)?**
+ New Relic: https://rpm.newrelic.com/accounts/532547/applications/8853774
+ BugSnag: https://bugsnag.com/docker/hub-prod/errors
+ PagerDuty: https://docker.pagerduty.com/services/PKZG21B
+
+ * **Do you use an exception tracking service (e.g. Bugsnag, Sentry, New Relic)?**
+ Yes, BugSnag and New Relic.
+
+ * **What’s the health check endpoint? And what checks does that endpoint perform?**
+ https://hub.docker.com/_health/
+
+ * **What external services do you depend on? How do you monitor them and handle failures?**
+ Hub API Gateway and all downstream Docker Cloud services.
+ Google Tag Manager (gtm.js)
+ Recurly (recurly.js)
+
+
+
+ * **What’s the link to view the logs?**
+
+ Alerting
+ --------
+
+ * **How do you know if your service is down?**
+ PagerDuty alerts
+ Prometheus alerts
+
+ * **What are the metrics that you alert on?**
+ 500's from Front End containers
+
+ * **Have you tested tripping on an alert to page somebody?**
+ Not manually tested. But production systems are paging properly.
+
+ * **What’s the link to your on-call schedule?**
+ https://docker.pagerduty.com/schedules#P88XAI9
+
+ * **Where is your on-call run-book?**
+ https://docker.atlassian.net/wiki/display/DE/Hub+UI+Runbook
+
+ Disaster
+ --------
+
+ * **What’s the plan if your persistence layer blows up?**
+ Front-end is stateless so this shouldn't be required, but restart Container if unsure.
+
+ * **What’s the plan if any of your external service dependencies blows up?**
+ Hub API Gateway or downstream service - find service owner, escalate/alert through PagerDuty, contact service team via Slack.
+ Google Tag Manager - disable Google Tag Manager from https://tagmanager.google.com - single signon with docker.com account
+ Recurly problem - check status.recurly.com, escalate/alert Billing team through PagerDuty, contact service team via Slack.
+ Update status.io describing impact to UI if any.
+
+
+ Security
+ --------
+
+ * **Is the service exposed on the public internet? Does it require TLS?**
+ https://hub.docker.com/
+
+ * **How do you store production secrets?**
+ Front-End does not store secrets. JWT is stored in user's browser cookie.
+
+ * **What is your authentication model (both user authentication and service-to-service authentication)?**
+ oauth
+
+ * **Do you store any sensitive user data (emails, phone numbers, intellectual property)?**
+ JWT in cookie.
+
+ Release process
+ ---------------
+
+ * **What’s the link to your docs on how to do a release?**
+ https://docker.atlassian.net/wiki/display/DH/Hub+frontend+Deployment+Process
+
+ * **How long does it take to release a code fix to production?**
+ 4-8 hours
diff --git a/startup-scripts/boot-dev-tmux.sh b/startup-scripts/boot-dev-tmux.sh
new file mode 100755
index 0000000000..0907330ac3
--- /dev/null
+++ b/startup-scripts/boot-dev-tmux.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+set -e
+
+eval $(docker-machine env dev)
+
+###############################################################
+# You must have `tmux` installed locally. On OSX, this can be #
+# accomplished with `brew install tmux` #
+###############################################################
+
+SESSION=HubDev
+DIR=${PWD##*/}_hub_1
+CONTAINER=$(sed s/-//g <<< $DIR)
+
+
+# Create new tmux session
+tmux -2 new-session -d -s $SESSION
+
+# Window 1
+
+## webpack task
+tmux split-window
+tmux select-pane -t 0
+tmux send-keys "DEBUG=* webpack -wd" C-m
+
+## styles
+
+tmux select-pane -t 1
+tmux send-keys "DEBUG=* gulp watch::styles::dev" C-m
+
+## Flow
+
+tmux split-window -h
+tmux select-pane -t 2
+tmux send-keys "flow" C-m
+
+## docker logs
+
+tmux select-pane -t 0
+tmux split-window -h
+tmux send-keys "docker-compose logs hub" C-m
+
+# Attach to session
+tmux -2 attach-session -t $SESSION
diff --git a/startup-scripts/boot-dev.sh b/startup-scripts/boot-dev.sh
new file mode 100755
index 0000000000..83b569ffe3
--- /dev/null
+++ b/startup-scripts/boot-dev.sh
@@ -0,0 +1,2 @@
+DEBUG=* webpack -dw &
+cd app/.build && nodemon ./server.js
diff --git a/startup-scripts/bootstrap-dev.sh b/startup-scripts/bootstrap-dev.sh
new file mode 100755
index 0000000000..454c4620fa
--- /dev/null
+++ b/startup-scripts/bootstrap-dev.sh
@@ -0,0 +1,8 @@
+# run this before the development container to bootstrap your local filesystem
+npm install
+cp app/favicon.ico app/.build/favicon.ico
+make server-target
+make styles-base
+gulp images::dev
+make images
+make docker-font-dev
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000000..9a5242e5ec
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,151 @@
+const debug = require('debug')('webpack-debug');
+var ENV_CONFIG = require('./_webpack/_envConfig.js');
+var fs = require('fs');
+var path = require('path');
+var ExtractTextPlugin = require("extract-text-webpack-plugin");
+var _ = require('lodash');
+var webpack = require('webpack');
+
+var loaders = require('./_webpack/_commonLoaders');
+
+/**
+ * blacklist this array from being included in `externals`.
+ *
+ * This has the effect of making any modules in this list be
+ * resolved at build time instead of runtime. This affects the
+ * server bundle
+ */
+var blacklist = ['.bin', 'hub-js-sdk', 'dux'];
+var node_modules = fs.readdirSync('node_modules').filter(function(x) {
+ return !_.includes(blacklist, x);
+});
+
+/* Dux Button Config */
+var elementButton = require('@dux/element-button/defaults');
+var buttons = elementButton.mkButtons([{
+ name: 'primary',
+ color: '#FFF',
+ bg: '#22B8EB'
+},{
+ name: 'secondary',
+ color: '#FFF',
+ bg: '#232C37'
+},{
+ name: 'coral',
+ color: '#FFF',
+ bg: '#FF85AF'
+},{
+ name: 'success',
+ color: '#FFF',
+ bg: '#0FD85A'
+},{
+ name: 'warning',
+ color: '#FFF',
+ bg: '#FF8546'
+},{
+ name: 'yellow',
+ color: '#FFF',
+ bg: '#FFDE50'
+},{
+ name: 'alert',
+ color: '#FFF',
+ bg: '#EB3E46'
+}]);
+debug('modules that will be runtime require dependencies of the server if the server requires them: ', node_modules);
+var commonConfig = {
+ resolve: {
+ extensions: ['', '.js', '.jsx', '.json'],
+ root: [
+ path.resolve(__dirname, './app/scripts/'),
+ path.resolve(__dirname, './app/scripts/components/')
+ ],
+ modulesDirectories: ['node_modules', 'app/scripts']
+ },
+ module: {
+ preLoaders: loaders.preLoaders,
+ loaders: loaders.commonLoaders
+ },
+ plugins: [
+ ENV_CONFIG,
+ new webpack.optimize.DedupePlugin(),
+ new ExtractTextPlugin('public/styles/style.css', { allChunks: true })
+ ],
+ postcss: [
+ require('postcss-import')(),
+ require('postcss-constants')({
+ defaults: _.merge(require('@dux/element-card/defaults')({
+ capBackground: '#f1f6fb',
+ borderColor: '#c4cdda'
+ }),
+ {
+ duxElementButton: {
+ radius: '.25rem',
+ buttons: buttons
+ }
+ })
+ }),
+ require('postcss-each'),
+ require('postcss-cssnext')({
+ browsers: 'last 2 versions',
+ features: {
+ // https://github.com/robwierzbowski/node-pixrem/issues/40
+ rem: false
+ }
+ }),
+ require('postcss-nested'),
+ require('lost')({
+ gutter: '1.25rem',
+ flexbox: 'flex'
+ }),
+ require('postcss-cssstats')(function(stats) {
+ /**
+ * this is in test-phase because it runs on all
+ * files individually. We should either figure out
+ * that that is useful or get it to run on the full postcss
+ * AST or extracted CSS file.
+ */
+ debug(stats);
+ }),
+ require('postcss-url')(),
+ require('cssnano')(),
+ require('postcss-browser-reporter')
+ ],
+ eslint: {
+ failOnError: true
+ },
+ profile: true
+}
+
+var clientBundle = _.assign({},
+ commonConfig,
+ {
+ // client.js
+ entry: './app/scripts/client.js',
+ devtool: 'eval-source-map',
+ output: {
+ path: 'app/.build/public/',
+ filename: 'js/client.js'
+ }
+ });
+
+var serverBundle = _.assign({},
+ commonConfig,
+ {
+ // server.js
+ entry: './app/scripts/server.js',
+ output: {
+ path: 'app/.build/',
+ filename: 'server.js',
+ libraryTarget: 'commonjs2'
+ },
+ target: 'node',
+ externals: node_modules,
+ node: {
+ __dirname: '/opt/hub/'
+ }
+ });
+
+module.exports = [
+ clientBundle,
+ serverBundle
+];