├── .babelrc ├── .cfignore ├── .circleci ├── _circle.yml_old └── config.yml ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierignore ├── CONTRIBUTING.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.md ├── Procfile ├── README.md ├── RELEASE.md ├── codecheck.sh ├── controllers ├── api.go ├── api_test.go ├── log.go ├── pprof │ └── pprof.go ├── root.go ├── root_test.go ├── routes.go ├── routes_test.go ├── secure.go ├── secure_test.go ├── uaa.go └── uaa_test.go ├── deploy ├── circle_deploy.sh ├── npm-version.sh └── vars-to-manifest.py ├── devtools ├── clean.sh ├── docker-setup.md ├── golang │ └── install_deps_then ├── manual-setup.md └── node │ ├── Dockerfile │ ├── cleanup.js │ ├── cleanup.sh │ ├── install_deps_then │ └── vnc_bg.png ├── docker-compose.yml ├── env.sample ├── helpers ├── env_vars.go ├── helpers.go ├── helpers_test.go ├── settings.go ├── settings_test.go ├── templates.go ├── templates_test.go ├── testdata │ └── templates │ │ ├── mail │ │ └── invite.html │ │ └── web │ │ └── index.html └── testhelpers │ ├── docker │ └── docker.go │ ├── mocks │ └── Mailer.go │ └── testhelpers.go ├── karma.conf.js ├── mailer ├── mailer.go └── mailer_test.go ├── manifests ├── manifest-base.yml ├── manifest-demo.yml ├── manifest-prod.yml └── manifest-staging.yml ├── npm-shrinkwrap.json ├── package.json ├── redirects └── cg-deck │ ├── README.md │ ├── manifest-ew.yml │ ├── manifest-govcloud.yml │ └── nginx.conf ├── server.go ├── static_src ├── actions │ ├── activity_actions.js │ ├── app_actions.js │ ├── domain_actions.js │ ├── env_actions.js │ ├── error_actions.js │ ├── form_actions.js │ ├── login_actions.js │ ├── org_actions.js │ ├── page_actions.js │ ├── quota_actions.js │ ├── route_actions.js │ ├── router_actions.js │ ├── service_actions.js │ ├── space_actions.js │ ├── upsi_actions.js │ └── user_actions.js ├── components │ ├── action.jsx │ ├── action │ │ ├── button.jsx │ │ └── link.jsx │ ├── activity_log.jsx │ ├── app_activity │ │ ├── app_activity.jsx │ │ ├── crash_event_item.jsx │ │ ├── log_item.jsx │ │ ├── raw_json_detail.jsx │ │ ├── route_event_item.jsx │ │ └── timestamp.jsx │ ├── app_container.jsx │ ├── app_count_status.jsx │ ├── app_list.jsx │ ├── app_quicklook.jsx │ ├── breadcrumbs │ │ ├── breadcrumbs.js │ │ ├── breadcrumbs_item.js │ │ └── index.js │ ├── complex_list.jsx │ ├── complex_list_item.jsx │ ├── confirmation_box.jsx │ ├── count_status.jsx │ ├── create_service_instance.jsx │ ├── dropdown.jsx │ ├── elastic_line.jsx │ ├── elastic_line_item.jsx │ ├── entity_empty.jsx │ ├── entity_icon.jsx │ ├── env │ │ ├── env_panel │ │ │ ├── env_panel.js │ │ │ ├── header.js │ │ │ ├── index.js │ │ │ └── section.js │ │ ├── env_var_form.js │ │ └── env_var_list_item.js │ ├── error_message.jsx │ ├── expandable_box.jsx │ ├── footer.jsx │ ├── form │ │ ├── form.jsx │ │ ├── form_element.jsx │ │ ├── form_error.jsx │ │ ├── form_number.jsx │ │ ├── form_select.jsx │ │ ├── form_text.jsx │ │ └── index.js │ ├── global_error.jsx │ ├── global_error_container.jsx │ ├── header │ │ ├── disclaimer.js │ │ ├── header.js │ │ ├── header_link.js │ │ └── index.js │ ├── icon.jsx │ ├── info_app_create.jsx │ ├── info_logs.jsx │ ├── loading.jsx │ ├── main_container.jsx │ ├── marketplace.jsx │ ├── not_found.jsx │ ├── notification.jsx │ ├── org_container.jsx │ ├── org_quicklook.jsx │ ├── overview_container.jsx │ ├── page_header.jsx │ ├── panel.jsx │ ├── panel_actions.jsx │ ├── panel_block.jsx │ ├── panel_documentation.jsx │ ├── panel_group.jsx │ ├── panel_header.jsx │ ├── panel_row.jsx │ ├── resource_usage.jsx │ ├── route.jsx │ ├── route_form.jsx │ ├── router │ │ └── route_provider.jsx │ ├── routes_panel.jsx │ ├── service_count_status.jsx │ ├── service_instance.jsx │ ├── service_instance_list.jsx │ ├── service_instance_panel.jsx │ ├── service_instance_table.jsx │ ├── service_instance_table_row.jsx │ ├── service_list.jsx │ ├── service_list_item.jsx │ ├── service_plan.jsx │ ├── service_plan_list.jsx │ ├── space_container.jsx │ ├── space_count_status.jsx │ ├── space_quicklook.jsx │ ├── stat.jsx │ ├── system_error_message.jsx │ ├── upsi │ │ └── upsi_panel.js │ ├── usage_and_limits.jsx │ ├── user_list.jsx │ ├── user_provider.jsx │ ├── user_role_control.jsx │ ├── user_role_list_control.jsx │ ├── users.jsx │ ├── users_invite.jsx │ └── users_selector.jsx ├── constants.js ├── css │ ├── main.css │ ├── overrides.css │ └── route_form.css ├── dispatcher.js ├── img │ └── dashboard-uaa-icon.jpg ├── main.js ├── models │ └── quicklook.js ├── routes.js ├── skins │ └── cg │ │ ├── angle-arrow-down-primary.svg │ │ ├── angle-arrow-up-primary-hover.svg │ │ ├── header.js │ │ ├── home_page │ │ ├── index.js │ │ ├── info_activities.jsx │ │ ├── info_environments.jsx │ │ ├── info_sandbox.jsx │ │ └── info_structure.jsx │ │ ├── icon-dot-gov.svg │ │ ├── icon-https.svg │ │ └── index.js ├── stores │ ├── activity_store.js │ ├── app_store.js │ ├── base_store.js │ ├── domain_store.js │ ├── env_store.js │ ├── error_store.js │ ├── form_store.js │ ├── login_store.js │ ├── org_store.js │ ├── page_store.js │ ├── quota_store.js │ ├── route_store.js │ ├── router_store.js │ ├── service_binding_store.js │ ├── service_instance_store.js │ ├── service_plan_store.js │ ├── service_store.js │ ├── space_store.js │ ├── upsi_store.js │ └── user_store.js ├── test │ ├── .eslintrc │ ├── functional │ │ ├── .eslintrc │ │ ├── global_error.spec.js │ │ ├── overview_quicklook.spec.js │ │ ├── pageobjects │ │ │ ├── base.element.js │ │ │ ├── breadcrumbs.element.js │ │ │ ├── global_errors.element.js │ │ │ ├── notification.element.js │ │ │ ├── org_quicklook.element.js │ │ │ ├── user_invite.element.js │ │ │ └── user_role.element.js │ │ ├── user_invite.spec.js │ │ └── user_role.spec.js │ ├── global_setup.js │ ├── perf │ │ ├── budgets.js │ │ ├── config.json │ │ ├── jasmine.json │ │ └── lighthouse-tests.spec.js │ ├── server │ │ ├── api.js │ │ ├── authstatus.js │ │ ├── fixtures │ │ │ ├── app_routes.js │ │ │ ├── app_stats.js │ │ │ ├── app_summaries.js │ │ │ ├── events.js │ │ │ ├── organization_memory_usage.js │ │ │ ├── organization_quota_definitions.js │ │ │ ├── organization_summaries.js │ │ │ ├── organization_user_roles.js │ │ │ ├── organization_users.js │ │ │ ├── organizations.js │ │ │ ├── service_bindings.js │ │ │ ├── service_instances.js │ │ │ ├── service_plans.js │ │ │ ├── services.js │ │ │ ├── shared_domains.js │ │ │ ├── space_quota_definitions.js │ │ │ ├── space_routes.js │ │ │ ├── space_summaries.js │ │ │ ├── space_user_roles.js │ │ │ ├── spaces.js │ │ │ ├── uaa_roles.js │ │ │ ├── user_association_responses.js │ │ │ ├── user_create_responses.js │ │ │ ├── user_invite_responses.js │ │ │ ├── user_organizations.js │ │ │ ├── user_role_org_add_new_role.js │ │ │ ├── user_roles.js │ │ │ └── user_spaces.js │ │ ├── index.js │ │ └── server.js │ ├── unit │ │ ├── actions │ │ │ ├── activity_actions.spec.js │ │ │ ├── app_actions.spec.js │ │ │ ├── domain_actions.spec.js │ │ │ ├── error_actions.spec.js │ │ │ ├── login_actions.spec.js │ │ │ ├── org_actions.spec.js │ │ │ ├── quota_actions.spec.js │ │ │ ├── route_actions.spec.js │ │ │ ├── router_actions.spec.js │ │ │ ├── service_actions.spec.js │ │ │ ├── space_actions.spec.js │ │ │ ├── upsi_actions.spec.js │ │ │ └── user_actions.spec.js │ │ ├── components │ │ │ ├── action.spec.jsx │ │ │ ├── action │ │ │ │ ├── button.spec.jsx │ │ │ │ └── link.spec.jsx │ │ │ ├── app_activity │ │ │ │ ├── crash_event_item.spec.jsx │ │ │ │ ├── log_item.spec.jsx │ │ │ │ ├── raw_json_detail.spec.jsx │ │ │ │ └── route_event_item.spec.jsx │ │ │ ├── create_service_instance.spec.jsx │ │ │ ├── header │ │ │ │ └── disclaimer.spec.js │ │ │ ├── route.spec.jsx │ │ │ ├── router │ │ │ │ └── route_provider.spec.jsx │ │ │ ├── service_instance.spec.jsx │ │ │ ├── service_instance_table.spec.jsx │ │ │ ├── service_instance_table_row.spec.js │ │ │ ├── service_list_item.spec.jsx │ │ │ ├── service_plan.spec.jsx │ │ │ ├── upsi │ │ │ │ └── upsi_panel.spec.js │ │ │ ├── user_list.spec.jsx │ │ │ ├── user_role_list_control.spec.jsx │ │ │ ├── users.spec.jsx │ │ │ ├── users_invite.spec.jsx │ │ │ └── users_selector.spec.jsx │ │ ├── helpers.js │ │ ├── routes.spec.js │ │ ├── stores │ │ │ ├── activity_store.spec.js │ │ │ ├── app_store.spec.js │ │ │ ├── base_store.spec.js │ │ │ ├── domain_store.spec.js │ │ │ ├── error_store.spec.js │ │ │ ├── login_store.spec.js │ │ │ ├── org_store.spec.js │ │ │ ├── quota_store.spec.js │ │ │ ├── route_store.spec.js │ │ │ ├── router_store.spec.js │ │ │ ├── service_binding_store.spec.js │ │ │ ├── service_instance_store.spec.js │ │ │ ├── service_plan_store.spec.js │ │ │ ├── service_store.spec.js │ │ │ ├── space_store.spec.js │ │ │ └── user_store.spec.js │ │ └── util │ │ │ ├── analytics.spec.js │ │ │ ├── cf_api.spec.js │ │ │ ├── format_bytes.spec.js │ │ │ ├── format_date.spec.js │ │ │ ├── format_route.spec.js │ │ │ ├── health.spec.js │ │ │ ├── loading_status.spec.js │ │ │ ├── poll.spec.js │ │ │ ├── uaa_api.spec.js │ │ │ ├── url.spec.js │ │ │ └── validators.spec.js │ └── webpack-karma-warnings-plugin.js ├── tests.bundle.js └── util │ ├── analytics.js │ ├── cf_api.js │ ├── element_id.js │ ├── event_log_types.js │ ├── format_bytes.js │ ├── format_date.js │ ├── format_route.js │ ├── health.js │ ├── loading_status.js │ ├── poll.js │ ├── uaa_api.js │ ├── url.js │ ├── validators.js │ └── window.js ├── templates ├── mail │ └── invite.html └── web │ └── index.html ├── wdio.conf.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.cov 4 | .env 5 | static/bower_components/ 6 | node_modules/ 7 | cf-deck 8 | cg-deck 9 | cg-dashboard 10 | /screenshots/ 11 | static/tests/ 12 | .org.chromium* 13 | # only exclude these index.html files from the VNC service. 14 | # they only appear in the root folder. 15 | /index.html* 16 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | plugins: 4 | govet: 5 | enabled: true 6 | golint: 7 | enabled: true 8 | nodesecurity: 9 | enabled: true 10 | eslint: 11 | enabled: false 12 | fixme: 13 | enabled: false 14 | csslint: 15 | enabled: false 16 | 17 | checks: 18 | similar-code: 19 | enabled: false 20 | identical-code: 21 | enabled: false 22 | 23 | exclude_patterns: 24 | - static_src/test/* 25 | - screenshots/ 26 | - "**/node_modules/" 27 | - "**/spec/" 28 | - "**/tests/" 29 | - "**/vendor/" 30 | - static/bower_components/ 31 | - static/assets/ 32 | - .DS_Store/ 33 | - helpers/debug.test 34 | 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb", "prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "ecmaFeatures": { 11 | "jsx": true, 12 | "experimentalObjectRestSpread": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/no-deprecated": 0, 19 | "import/no-unresolved": [2, { "ignore": ["skin", "dashboard"] }], 20 | "react/prefer-stateless-function": 0, 21 | // TODO(jonathaningram): the following rules are turned off to quiet the 22 | // linter during a transition between an old version of airbnb/javascript, 23 | // introducing prettier and upgrading to the latest version of 24 | // airbnb/javascript. 25 | // The existence of these rules does not indicate that they should be 26 | // ignored. It simply means that they should be re-enabled as the codebase 27 | // is updated. 28 | "import/imports-first": 0, 29 | "import/no-extraneous-dependencies": 0, 30 | "import/no-named-as-default": 0, 31 | "react/jsx-filename-extension": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.cov 4 | .env 5 | /screenshots/ 6 | static/bower_components/ 7 | node_modules/ 8 | npm-debug.log 9 | coverage 10 | .godeps/ 11 | Godeps/_workspace/ 12 | cg-deck 13 | cg-dashboard 14 | static/assets 15 | vendor/* 16 | !vendor/vendor.json 17 | .DS_Store 18 | context/selenium-logs/selenium-standalone.txt 19 | # useful for template tests 20 | *.returned 21 | .org.chromium* 22 | # only exclude these index.html files from the VNC service. 23 | # they only appear in the root folder. 24 | /index.html* 25 | debug.test 26 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.7 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | npm-shrinkwrap.json 2 | package.json 3 | package-lock.json 4 | static 5 | vendor 6 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/boj/redistore" 26 | version = "~1.2.0" 27 | 28 | [[constraint]] 29 | name = "github.com/cloudfoundry-community/go-cfenv" 30 | 31 | [[constraint]] 32 | name = "github.com/cloudfoundry/loggregatorlib" 33 | 34 | [[constraint]] 35 | name = "github.com/fsouza/go-dockerclient" 36 | 37 | [[constraint]] 38 | name = "github.com/garyburd/redigo" 39 | 40 | [[constraint]] 41 | name = "github.com/gocraft/web" 42 | 43 | [[constraint]] 44 | name = "github.com/gogo/protobuf" 45 | 46 | [[constraint]] 47 | name = "github.com/gorilla/context" 48 | 49 | [[constraint]] 50 | name = "github.com/gorilla/csrf" 51 | version = "~1.4.0" 52 | 53 | [[constraint]] 54 | name = "github.com/gorilla/sessions" 55 | 56 | [[constraint]] 57 | name = "github.com/govau/cf-common" 58 | version = "0.0.2" 59 | 60 | [[constraint]] 61 | name = "github.com/jordan-wright/email" 62 | 63 | [[constraint]] 64 | name = "github.com/ory/dockertest" 65 | version = "3.0.7" 66 | 67 | [[constraint]] 68 | name = "github.com/satori/go.uuid" 69 | version = "1.1.0" 70 | 71 | [[constraint]] 72 | name = "github.com/stretchr/testify" 73 | 74 | [[constraint]] 75 | name = "github.com/yvasiyarov/gorelic" 76 | 77 | [[constraint]] 78 | name = "golang.org/x/net" 79 | 80 | [[constraint]] 81 | name = "golang.org/x/oauth2" 82 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the public domain 2 | within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work worldwide 5 | through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full 10 | text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 11 | 12 | ### No Copyright 13 | 14 | The person who associated a work with this deed has dedicated the work to the 15 | public domain by waiving all of his or her rights to the work worldwide under 16 | copyright law, including all related and neighboring rights, to the extent 17 | allowed by law. 18 | 19 | You can copy, modify, distribute and perform the work, even for commercial 20 | purposes, all without asking permission. 21 | 22 | ### Other Information 23 | 24 | In no way are the patent or trademark rights of any person affected by CC0, nor 25 | are the rights that other persons may have in the work or in how the work is 26 | used, such as publicity or privacy rights. 27 | 28 | Unless expressly stated otherwise, the person who associated a work with this 29 | deed makes no warranties about the work, and disclaims liability for all uses of 30 | the work, to the fullest extent permitted by applicable law. When using or 31 | citing the work, you should not imply endorsement by the author or the affirmer. 32 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: cg-dashboard -port=$PORT 2 | -------------------------------------------------------------------------------- /controllers/api.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gocraft/web" 6 | "net/http" 7 | ) 8 | 9 | // APIContext stores the session info and access token per user. 10 | // All routes within APIContext represent the API routes 11 | type APIContext struct { 12 | *SecureContext // Required. 13 | } 14 | 15 | // APIProxy is a handler that serves as a proxy for all the CF API. Any route that comes in the /v2/* route 16 | // that has not been specified, will just come here. 17 | func (c *APIContext) APIProxy(rw web.ResponseWriter, req *web.Request) { 18 | reqURL := fmt.Sprintf("%s%s", c.Settings.ConsoleAPI, req.URL) 19 | c.Proxy(rw, req.Request, reqURL, c.GenericResponseHandler) 20 | } 21 | 22 | // UserProfile redirects users to the `/profile` page 23 | func (c *APIContext) UserProfile(rw web.ResponseWriter, req *web.Request) { 24 | profileURL := fmt.Sprintf("%s%s", c.Settings.LoginURL, "/profile") 25 | http.Redirect(rw, req.Request, profileURL, http.StatusFound) 26 | } 27 | 28 | // AuthStatus simply returns authorized. This endpoint is just a quick endpoint to indicate that if a 29 | // user can reach here after passing through the OAuth Middleware, they are authorized. 30 | func (c *APIContext) AuthStatus(rw web.ResponseWriter, req *web.Request) { 31 | rw.Write([]byte("{\"status\": \"authorized\"}")) 32 | } 33 | -------------------------------------------------------------------------------- /controllers/api_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | . "github.com/18F/cg-dashboard/helpers/testhelpers" 5 | 6 | "testing" 7 | ) 8 | 9 | var authStatusTests = []BasicSecureTest{ 10 | { 11 | BasicConsoleUnitTest: BasicConsoleUnitTest{ 12 | TestName: "Basic Authorized Status Session", 13 | EnvVars: GetMockCompleteEnvVars(), 14 | SessionData: ValidTokenData, 15 | }, 16 | ExpectedResponse: NewJSONResponseContentTester("{\"status\": \"authorized\"}"), 17 | }, 18 | } 19 | 20 | func TestAuthStatus(t *testing.T) { 21 | for _, test := range authStatusTests { 22 | // Create request 23 | response, request := NewTestRequest("GET", "/v2/authstatus", nil) 24 | 25 | router, _ := CreateRouterWithMockSession(test.SessionData, test.EnvVars) 26 | router.ServeHTTP(response, request) 27 | if !test.ExpectedResponse.Check(t, response.Body.String()) { 28 | t.Errorf("Expected %s. Found %s\n", test.ExpectedResponse.Display(), response.Body.String()) 29 | } 30 | } 31 | } 32 | 33 | var profileTests = []BasicSecureTest{ 34 | { 35 | BasicConsoleUnitTest: BasicConsoleUnitTest{ 36 | TestName: "Basic Authorized Profile", 37 | EnvVars: GetMockCompleteEnvVars(), 38 | SessionData: ValidTokenData, 39 | }, 40 | ExpectedResponse: NewStringContentTester("https://loginurl/profile"), 41 | }, 42 | } 43 | 44 | func TestProfile(t *testing.T) { 45 | for _, test := range profileTests { 46 | // Create request 47 | response, request := NewTestRequest("GET", "/v2/profile", nil) 48 | 49 | router, _ := CreateRouterWithMockSession(test.SessionData, test.EnvVars) 50 | router.ServeHTTP(response, request) 51 | if !test.ExpectedResponse.Check(t, response.Header().Get("location")) { 52 | t.Errorf("Profile route does not redirect to loginurl profile page") 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /deploy/circle_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will install the autopilot plugin, login, pick the right manifest and deploy the app with 0 downtime. 4 | 5 | set -e 6 | set -o pipefail 7 | 8 | # Install cf cli 9 | curl -v -L -o cf-cli_amd64.deb 'https://cli.run.pivotal.io/stable?release=debian64&source=github' 10 | sudo dpkg -i cf-cli_amd64.deb 11 | cf -v 12 | 13 | # Install autopilot plugin for blue-green deploys 14 | go get github.com/contraband/autopilot 15 | cf install-plugin -f /home/ubuntu/.go_workspace/bin/autopilot 16 | 17 | # Note: Spaces and deployer account username are the same in different environments. 18 | # Only the organization, api, deployer account password differ. 19 | 20 | 21 | CF_APP="cg-dashboard" 22 | CF_SPACE="dashboard" 23 | 24 | if [[ "$CIRCLE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9-]+)? ]] 25 | then 26 | CF_MANIFEST="manifest-prod.yml" 27 | CF_API="https://api.fr.cloud.gov" 28 | CF_USERNAME=$CF_USERNAME_PROD_SPACE 29 | CF_PASSWORD=$CF_PASSWORD_PROD_SPACE 30 | elif [ "$CIRCLE_BRANCH" == "master" ] 31 | then 32 | CF_MANIFEST="manifest-staging.yml" 33 | CF_API="https://api.fr-stage.cloud.gov" 34 | CF_USERNAME=$CF_USERNAME_STAGE_SPACE 35 | CF_PASSWORD=$CF_PASSWORD_STAGE_SPACE 36 | elif [ "$CIRCLE_BRANCH" == "demo" ] 37 | then 38 | CF_MANIFEST="manifest-demo.yml" 39 | CF_APP="cg-dashboard-demo" 40 | else 41 | echo Unknown environment, quitting. >&2 42 | exit 0 43 | fi 44 | 45 | echo manifest: $CF_MANIFEST 46 | echo space: $CF_SPACE 47 | 48 | function deploy () { 49 | local manifest=${1} 50 | local org=${2} 51 | local space=${3} 52 | local app=${4} 53 | 54 | # Log in 55 | cf api $CF_API 56 | cf auth $CF_USERNAME $CF_PASSWORD 57 | cf target -o $org -s $space 58 | 59 | # Run autopilot plugin 60 | cf zero-downtime-push $app -f $manifest 61 | } 62 | 63 | # Set manifest path 64 | MANIFEST_PATH=manifests/$CF_MANIFEST 65 | deploy "$MANIFEST_PATH" "$CF_ORGANIZATION" "$CF_SPACE" "$CF_APP" 66 | -------------------------------------------------------------------------------- /deploy/npm-version.sh: -------------------------------------------------------------------------------- 1 | PACKAGE_VERSION=$(cat package.json \ 2 | | grep version \ 3 | | head -1 \ 4 | | awk -F: '{ print $2 }' \ 5 | | sed 's/[",]//g' \ 6 | | tr -d '[[:space:]]') 7 | 8 | echo $PACKAGE_VERSION 9 | -------------------------------------------------------------------------------- /deploy/vars-to-manifest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Run from root of project. e.g. python deploy/travis-vars-to-manifest.py 4 | import ruamel.yaml as yaml 5 | import os 6 | import sys 7 | 8 | BUILD_INFO = "BUILD_INFO" 9 | 10 | yaml_data = None 11 | # Read the manifest file 12 | with open('manifests/manifest-base.yml', 'r') as base_manifest: 13 | data = base_manifest.read() 14 | yaml_data = yaml.load(data, yaml.RoundTripLoader) 15 | 16 | build_info = "" + str(os.environ.get(BUILD_INFO)) 17 | if len(build_info) < 1 or build_info == "None": 18 | print BUILD_INFO + " is empty" 19 | sys.exit(1) 20 | 21 | # Put the environment vars into place. 22 | yaml_data['env'][BUILD_INFO] = build_info 23 | 24 | # Overwrite the manifest file. 25 | with open('manifests/manifest-base.yml', 'w') as base_manifest: 26 | base_manifest.write("---\n") 27 | base_manifest.write(yaml.dump(yaml_data, Dumper=yaml.RoundTripDumper)) 28 | -------------------------------------------------------------------------------- /devtools/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Useful for resetting your local environment 4 | 5 | docker stop $(docker ps -a -q) 6 | docker rm $(docker ps -a -q) 7 | docker volume rm -f $(docker volume ls -q) 8 | rm -rf vendor 9 | rm -rf node_modules 10 | -------------------------------------------------------------------------------- /devtools/golang/install_deps_then: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -x $GOPATH/bin/dep ]; then 4 | go get -u github.com/golang/dep/cmd/dep 5 | fi 6 | 7 | export PATH=$GOPATH/bin:$PATH 8 | if [ ! -x vendor ]; then 9 | dep ensure 10 | fi 11 | 12 | prgm=$1 13 | shift; 14 | bash -i -c "$prgm $@" 15 | -------------------------------------------------------------------------------- /devtools/node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM consol/ubuntu-xfce-vnc 2 | 3 | USER 0 4 | # need java for selenium 5 | # need bzip2 for downloading phantomjs for cg-style https://github.com/Medium/phantomjs/issues/630 6 | RUN apt-get update && apt-get install -y -q --no-install-recommends \ 7 | bzip2 \ 8 | default-jre \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | USER 0 12 | 13 | # let's change the background picture. 14 | COPY ./devtools/node/vnc_bg.png /headless/.config/bg_sakuli.png 15 | 16 | ENV NVM_DIR $HOME/.nvm 17 | # change this version. 18 | ENV NODE_VERSION "v6.7.0" 19 | ENV NODE_PATH "$NVM_DIR/versions/node/$NODE_VERSION/lib/node_modules" 20 | ENV PATH "$NVM_DIR/versions/node/$NODE_VERSION/bin:$PATH" 21 | -------------------------------------------------------------------------------- /devtools/node/cleanup.js: -------------------------------------------------------------------------------- 1 | const chokidar = require("chokidar"); 2 | const fs = require("fs"); 3 | 4 | // Create a watcher to delete weird files created by the docker containers. 5 | // Files: 6 | // index.html - created from noVNC 7 | chokidar.watch(["^index.html*"]).on("add", file => { 8 | fs.unlinkSync(`${file}`); 9 | }); 10 | -------------------------------------------------------------------------------- /devtools/node/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # We don't need this for regular development. only in the docker container. 4 | npm install chokidar 5 | 6 | node devtools/node/cleanup.js 7 | -------------------------------------------------------------------------------- /devtools/node/install_deps_then: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f $NVM_DIR/nvm.sh ]; then 4 | echo "Downloading nvm." 5 | bash -i -c "wget -O - https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash" 6 | fi 7 | bash -i -c "nvm install $NODE_VERSION && nvm alias default && nvm use default" 8 | 9 | if [ ! -x node_modules/.bin/deps-ok ]; then 10 | npm install deps-ok 11 | fi 12 | 13 | ./node_modules/.bin/deps-ok &> /dev/null 14 | if [ $? -ne 0 ]; then 15 | echo "Missing dependencies. Running npm install. Please wait..." 16 | npm install 17 | fi 18 | 19 | if [[ -z "${CG_STLYE}" ]]; then 20 | if [ ! -x node_modules/selenium-standalone/.selenium/selenium-server ]; then 21 | echo "Finishing the selenium install." 22 | npm run test-selenium-install 23 | fi 24 | else 25 | echo "In cg-style repo. no extra commands to run." 26 | fi 27 | 28 | 29 | if [[ -z "${START_VNC}" ]]; then 30 | echo "Not starting VNC..." 31 | else 32 | # make sure we comment out the nvm bash_completion line because 33 | # it doesn't work when starting VNC. 34 | sed -i 's/.*NVM_DIR\/bash_completion/#&/' $HOME/.bashrc 35 | # in the case of wait, we will want to setup the VNC. 36 | echo "Starting VNC service..." 37 | /dockerstartup/vnc_startup.sh &> /dev/null 38 | if [ $? -ne 0 ]; then 39 | # if it fails, we should exit before getting caught in the sleep command. 40 | echo "Something went wrong setting up vnc" 41 | exit 1 42 | fi 43 | echo "VNC service started!" 44 | fi 45 | 46 | echo "Running '$@'" 47 | exec "$@" 48 | -------------------------------------------------------------------------------- /devtools/node/vnc_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloud-gov/cg-dashboard/6675bf8e832eb99e79cce73ed8a62dd940ea83aa/devtools/node/vnc_bg.png -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # The absolute path to your project root. If you followed the cloning instructions 2 | # above, this path should end with `cg-dashboard-ws` 3 | export GOPATH= 4 | 5 | # Registered client id with UAA. 6 | export CONSOLE_CLIENT_ID= 7 | 8 | # The client secret. 9 | export CONSOLE_CLIENT_SECRET= 10 | 11 | # The URL of the service itself. 12 | export CONSOLE_HOSTNAME=http://localhost:9999 13 | 14 | # The base URL of the auth service. 15 | export CONSOLE_LOGIN_URL=https://login.fr.cloud.gov 16 | 17 | # The URL of the UAA service. 18 | export CONSOLE_UAA_URL=https://uaa.fr.cloud.gov 19 | 20 | # The URL of the API service. 21 | export CONSOLE_API_URL=https://api.fr.cloud.gov 22 | 23 | # The URL of the loggregator service. 24 | export CONSOLE_LOG_URL=https://loggregator.fr.cloud.gov 25 | 26 | # The key used to protect session data 27 | export CSRF_KEY="$(openssl rand -hex 32)" 28 | 29 | # The key used to protect session data 30 | export SESSION_AUTHENTICATION_KEY="$(openssl rand -hex 64)" 31 | 32 | # If set to `true` or `1`, will turn on `/debug/pprof` endpoints as seen [here](https://golang.org/pkg/net/http/pprof/) 33 | # export PPROF_ENABLED=true 34 | 35 | # The absolute path to your `cg-style` repo. If set, will use a local 36 | # copy of `cloudgov-style` to build the front end application. 37 | # export CG_STYLE_PATH= 38 | 39 | # If set to `true` or `1`, will set the `secure` flag on session cookies 40 | export SECURE_COOKIES=true 41 | 42 | # If set to `true` or `1`, will indicate that we are using a 43 | # development Cloud Foundry deployment. 44 | # needed before anything insecure can be used (e.g. insecure cookies.) 45 | # export LOCAL_CF=0 46 | 47 | # The name of the skin to use (defaults to cg) 48 | # export SKIN_NAME=cg 49 | 50 | # The SMTP HOST for mailcatcher 51 | export SMTP_HOST=localhost 52 | 53 | # The SMTP PORT 54 | export SMTP_PORT=1025 55 | 56 | # The SMTP USER 57 | export SMTP_USER= 58 | 59 | # The SMTP PASS 60 | export SMTP_PASS= 61 | 62 | # The FROM Address for email 63 | export SMTP_FROM='no-reply@cloud.gov' 64 | 65 | # The New Relic ID 66 | export NEW_RELIC_ID=12345 67 | 68 | # The New Relic Browser License ID 69 | export NEW_RELIC_BROWSER_LICENSE_KEY=abcdef -------------------------------------------------------------------------------- /helpers/helpers_test.go: -------------------------------------------------------------------------------- 1 | package helpers_test 2 | 3 | import ( 4 | "net/http/httptest" 5 | 6 | "github.com/18F/cg-dashboard/helpers" 7 | "github.com/18F/cg-dashboard/helpers/testhelpers" 8 | 9 | "net/http" 10 | "testing" 11 | ) 12 | 13 | type tokenTestData struct { 14 | testName string 15 | sessionName string 16 | sessionData map[string]interface{} 17 | returnValueNull bool 18 | } 19 | 20 | var getValidTokenTests = []tokenTestData{ 21 | { 22 | testName: "Basic Valid Token Check", 23 | sessionData: testhelpers.ValidTokenData, 24 | returnValueNull: false, 25 | }, 26 | { 27 | testName: "Basic Invalid Token Check", 28 | sessionData: testhelpers.InvalidTokenData, 29 | returnValueNull: true, 30 | }, 31 | { 32 | testName: "No Session Data Check", 33 | sessionName: "", 34 | returnValueNull: true, 35 | }, 36 | { 37 | testName: "Nil Session Check", 38 | sessionName: "nilSession", 39 | returnValueNull: true, 40 | }, 41 | } 42 | 43 | func TestGetValidToken(t *testing.T) { 44 | mockRequest, _ := http.NewRequest("GET", "", nil) 45 | mockSettings := helpers.Settings{} 46 | mockResponse := httptest.NewRecorder() 47 | 48 | for _, test := range getValidTokenTests { 49 | // Initialize a new session store. 50 | store := testhelpers.MockSessionStore{} 51 | store.ResetSessionData(test.sessionData, test.sessionName) 52 | mockSettings.Sessions = store 53 | 54 | value := helpers.GetValidToken(mockRequest, mockResponse, &mockSettings) 55 | if (value == nil) == test.returnValueNull { 56 | } else { 57 | t.Errorf("Test %s did not meet expected value. Expected: %t. Actual: %t\n", test.testName, test.returnValueNull, (value == nil)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /helpers/testhelpers/mocks/Mailer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import mailer "github.com/18F/cg-dashboard/mailer" 4 | import mock "github.com/stretchr/testify/mock" 5 | 6 | // Mailer is an autogenerated mock type for the Mailer type 7 | type Mailer struct { 8 | mock.Mock 9 | } 10 | 11 | // SendEmail provides a mock function with given fields: emailAddress, subject, body 12 | func (_m *Mailer) SendEmail(emailAddress string, subject string, body []byte) error { 13 | ret := _m.Called(emailAddress, subject, body) 14 | 15 | var r0 error 16 | if rf, ok := ret.Get(0).(func(string, string, []byte) error); ok { 17 | r0 = rf(emailAddress, subject, body) 18 | } else { 19 | r0 = ret.Error(0) 20 | } 21 | 22 | return r0 23 | } 24 | 25 | var _ mailer.Mailer = (*Mailer)(nil) 26 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jul 27 2015 12:17:38 GMT-0400 (EDT) 3 | 4 | const webpackConfig = require("./webpack.config"); 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | browsers: ["Chrome"], 9 | 10 | frameworks: ["jasmine", "jasmine-matchers", "jasmine-sinon"], 11 | 12 | files: ["./static_src/tests.bundle.js"], 13 | 14 | exclude: [], 15 | 16 | plugins: [ 17 | "karma-chrome-launcher", 18 | "karma-jasmine", 19 | "karma-jasmine-matchers", 20 | "karma-jasmine-sinon", 21 | "karma-sourcemap-loader", 22 | "karma-webpack" 23 | ], 24 | 25 | preprocessors: { 26 | "static_src/tests.bundle.js": ["webpack"] 27 | }, 28 | 29 | webpack: webpackConfig, 30 | 31 | reporters: ["progress"], 32 | 33 | client: { 34 | captureConsole: process.env.CAPTURE_TEST_CONSOLE || false 35 | }, 36 | 37 | port: 9876, 38 | 39 | colors: true, 40 | 41 | logLevel: config.LOG_ERROR, 42 | 43 | autoWatch: true, 44 | 45 | singleRun: false 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /mailer/mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "net/smtp" 7 | 8 | "github.com/18F/cg-dashboard/helpers" 9 | "github.com/jordan-wright/email" 10 | ) 11 | 12 | // Mailer is a interface that any mailer should implement. 13 | type Mailer interface { 14 | SendEmail(emailAddress string, subject string, body []byte) error 15 | } 16 | 17 | // InitSMTPMailer creates a new SMTP Mailer 18 | func InitSMTPMailer(settings helpers.Settings) (Mailer, error) { 19 | var tlsConfig *tls.Config 20 | if settings.SMTPCert != "" { 21 | pool := x509.NewCertPool() 22 | pool.AppendCertsFromPEM([]byte(settings.SMTPCert)) 23 | tlsConfig = &tls.Config{ 24 | ServerName: settings.SMTPHost, 25 | RootCAs: pool, 26 | } 27 | 28 | } 29 | return &smtpMailer{ 30 | smtpHost: settings.SMTPHost, 31 | smtpPort: settings.SMTPPort, 32 | smtpUser: settings.SMTPUser, 33 | smtpPass: settings.SMTPPass, 34 | smtpFrom: settings.SMTPFrom, 35 | smtpCert: settings.SMTPCert, 36 | tlsConfig: tlsConfig, 37 | }, nil 38 | } 39 | 40 | type smtpMailer struct { 41 | smtpHost string 42 | smtpPort string 43 | smtpUser string 44 | smtpPass string 45 | smtpFrom string 46 | smtpCert string 47 | tlsConfig *tls.Config 48 | } 49 | 50 | func (s *smtpMailer) SendEmail(emailAddress, subject string, body []byte) error { 51 | e := email.NewEmail() 52 | e.From = s.smtpFrom 53 | e.To = []string{" <" + emailAddress + ">"} 54 | e.HTML = body 55 | e.Subject = subject 56 | 57 | addr := s.smtpHost + ":" + s.smtpPort 58 | auth := smtp.PlainAuth("", s.smtpUser, s.smtpPass, s.smtpHost) 59 | 60 | if s.tlsConfig != nil { 61 | return e.SendWithTLS(addr, auth, s.tlsConfig) 62 | } 63 | return e.Send(s.smtpHost+":"+s.smtpPort, smtp.PlainAuth("", s.smtpUser, s.smtpPass, s.smtpHost)) 64 | } 65 | -------------------------------------------------------------------------------- /mailer/mailer_test.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/18F/cg-dashboard/helpers" 9 | . "github.com/18F/cg-dashboard/helpers/testhelpers/docker" 10 | ) 11 | 12 | func TestSendEmail(t *testing.T) { 13 | hostname, smtpPort, apiPort, cleanup := CreateTestMailCatcher(t) 14 | // Test InitSMTPMailer with valid path for templates. 15 | settings := helpers.Settings{ 16 | SMTPHost: hostname, 17 | SMTPPort: smtpPort, 18 | SMTPFrom: "test@dashboard.com", 19 | } 20 | mailer, err := InitSMTPMailer(settings) 21 | if mailer == nil { 22 | t.Error("Expected non nil mailer.") 23 | } 24 | if err != nil { 25 | t.Errorf("Expected nil error, found %s", err.Error()) 26 | } 27 | body := bytes.NewBufferString("test html here") 28 | err = mailer.SendEmail("test@receiver.com", "sample subject", body.Bytes()) 29 | if err != nil { 30 | t.Errorf("Expected nil error, found %s", err.Error()) 31 | } 32 | receivedData, err := GetLatestMailMessageData(hostname, apiPort) 33 | if err != nil { 34 | t.Errorf("Expected nil error, found %s", err.Error()) 35 | } 36 | if !strings.Contains(string(receivedData), `"sender":""`) { 37 | t.Error("Expected to find sender metadata") 38 | } 39 | if !strings.Contains(string(receivedData), `"recipients":[""]`) { 40 | t.Error("Expected to find receipient metadata") 41 | } 42 | if !strings.Contains(string(receivedData), `"subject":"sample subject"`) { 43 | t.Error("Expected to find receipient metadata") 44 | } 45 | // Useful for generating test data. Undo to learn more about the data. 46 | // log.Println(string(receivedData)) 47 | 48 | cleanup() // Destroy the mail server. 49 | 50 | // Try sending mail to bad server. 51 | err = mailer.SendEmail("test@receiver.com", "sample subject", body.Bytes()) 52 | if err == nil { 53 | t.Error("Expected non nil error") 54 | } 55 | } 56 | 57 | func TestInitSMTPMailer(t *testing.T) { 58 | settings := helpers.Settings{} 59 | mailer, err := InitSMTPMailer(settings) 60 | if mailer == nil { 61 | t.Error("Expected non nil mailer.") 62 | } 63 | if err != nil { 64 | t.Errorf("Expected nil error, found %s", err.Error()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /manifests/manifest-base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | buildpack: go_buildpack 3 | memory: 256M 4 | services: 5 | - dashboard-ups 6 | env: 7 | SECURE_COOKIES: true 8 | GA_TRACKING_ID: UA-48605964-34 9 | GOVERSION: go1.12 10 | GOPACKAGENAME: github.com/18F/cg-dashboard 11 | -------------------------------------------------------------------------------- /manifests/manifest-demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit: manifest-base.yml 3 | applications: 4 | - name: cg-dashboard-demo 5 | host: dashboard-demo 6 | env: 7 | CONSOLE_HOSTNAME: https://dashboard-demo.app.cloud.gov 8 | -------------------------------------------------------------------------------- /manifests/manifest-prod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit: manifest-base.yml 3 | domain: fr.cloud.gov 4 | applications: 5 | - name: cg-dashboard-deprecated 6 | host: dashboard-deprecated 7 | instances: 3 8 | env: 9 | CONSOLE_HOSTNAME: https://dashboard-deprecated.fr.cloud.gov 10 | CONSOLE_LOGIN_URL: https://login.fr.cloud.gov 11 | CONSOLE_UAA_URL: https://uaa.fr.cloud.gov 12 | CONSOLE_API_URL: https://api.fr.cloud.gov 13 | CONSOLE_LOG_URL: https://loggregator.fr.cloud.gov 14 | -------------------------------------------------------------------------------- /manifests/manifest-staging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit: manifest-base.yml 3 | domain: fr-stage.cloud.gov 4 | applications: 5 | - name: cg-dashboard-deprecated 6 | host: dashboard-deprecated 7 | instances: 3 8 | env: 9 | CONSOLE_HOSTNAME: https://dashboard-deprecated.fr-stage.cloud.gov 10 | CONSOLE_LOGIN_URL: https://login.fr-stage.cloud.gov 11 | CONSOLE_UAA_URL: https://uaa.fr-stage.cloud.gov 12 | CONSOLE_API_URL: https://api.fr-stage.cloud.gov 13 | CONSOLE_LOG_URL: https://loggregator.fr-stage.cloud.gov 14 | -------------------------------------------------------------------------------- /redirects/cg-deck/README.md: -------------------------------------------------------------------------------- 1 | # cf-redirect 2 | 3 | Small static app to redirect traffic from one domain to another. 4 | 5 | ## Deploy 6 | 7 | This redirect is deployed to east/west and govcloud. 8 | 9 | Govcloud redirects console.fr.cloud.gov -> dashboard.fr.cloud.gov 10 | 11 | $ cf target -o cloud-gov -s deck 12 | $ cf zero-downtime-push console-redirect -f redirects/cg-deck/manifest-govcloud.yml 13 | 14 | East/west redirects console.cloud.gov -> dashboard.cloud.gov 15 | 16 | $ cf target -o cf -s dashboard-prod 17 | $ cf zero-downtime-push console-redirect -f redirects/cg-deck/manifest-ew.yml 18 | -------------------------------------------------------------------------------- /redirects/cg-deck/manifest-ew.yml: -------------------------------------------------------------------------------- 1 | --- 2 | buildpack: staticfile_buildpack 3 | applications: 4 | - memory: 64MB 5 | name: console-redirect 6 | domain: console.cloud.gov 7 | no-hostname: true 8 | path: . 9 | env: 10 | TARGET_DOMAIN: dashboard.cloud.gov 11 | -------------------------------------------------------------------------------- /redirects/cg-deck/manifest-govcloud.yml: -------------------------------------------------------------------------------- 1 | --- 2 | buildpack: staticfile_buildpack 3 | applications: 4 | - memory: 64MB 5 | name: console-redirect 6 | host: console 7 | domain: fr.cloud.gov 8 | path: . 9 | env: 10 | TARGET_DOMAIN: dashboard.fr.cloud.gov 11 | -------------------------------------------------------------------------------- /redirects/cg-deck/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | daemon off; 3 | 4 | error_log <%= ENV["APP_ROOT"] %>/nginx/logs/error.log; 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | server { 9 | listen <%= ENV["PORT"] %>; 10 | set $target_domain <%= ENV["TARGET_DOMAIN"] %>; 11 | server_name localhost; 12 | 13 | expires 3600; 14 | return 301 https://$target_domain$request_uri; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static_src/actions/activity_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from "../dispatcher.js"; 2 | import { activityActionTypes } from "../constants"; 3 | import cfApi from "../util/cf_api.js"; 4 | 5 | const activityActions = { 6 | fetchSpaceEvents(spaceGuid, appGuid) { 7 | AppDispatcher.handleViewAction({ 8 | type: activityActionTypes.EVENTS_FETCH, 9 | spaceGuid 10 | }); 11 | 12 | return cfApi 13 | .fetchSpaceEvents(spaceGuid, { appGuid }) 14 | .then(activityActions.receivedSpaceEvents); 15 | }, 16 | 17 | receivedSpaceEvents(events) { 18 | AppDispatcher.handleServerAction({ 19 | type: activityActionTypes.EVENTS_RECEIVED, 20 | events 21 | }); 22 | 23 | return Promise.resolve(events); 24 | }, 25 | 26 | fetchAppLogs(appGuid) { 27 | AppDispatcher.handleViewAction({ 28 | type: activityActionTypes.LOGS_FETCH, 29 | appGuid 30 | }); 31 | 32 | return cfApi 33 | .fetchAppLogs(appGuid) 34 | .then(logs => activityActions.receivedAppLogs(appGuid, logs)) 35 | .catch(err => activityActions.errorAppLogs(appGuid, err)); 36 | }, 37 | 38 | receivedAppLogs(appGuid, logs) { 39 | AppDispatcher.handleServerAction({ 40 | type: activityActionTypes.LOGS_RECEIVED, 41 | appGuid, 42 | logs 43 | }); 44 | 45 | return Promise.resolve(logs); 46 | }, 47 | 48 | errorAppLogs(appGuid, err) { 49 | AppDispatcher.handleServerAction({ 50 | type: activityActionTypes.LOGS_ERROR, 51 | appGuid, 52 | err 53 | }); 54 | 55 | return Promise.resolve(); 56 | } 57 | }; 58 | 59 | export default activityActions; 60 | -------------------------------------------------------------------------------- /static_src/actions/domain_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for domain entities. Any actions such as fetching, creating, 3 | * etc should go here. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher.js"; 7 | import { domainActionTypes } from "../constants"; 8 | 9 | export default { 10 | fetch(domainGuid) { 11 | AppDispatcher.handleViewAction({ 12 | type: domainActionTypes.DOMAIN_FETCH, 13 | domainGuid 14 | }); 15 | }, 16 | 17 | receivedDomain(domain) { 18 | AppDispatcher.handleServerAction({ 19 | type: domainActionTypes.DOMAIN_RECEIVED, 20 | domain 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /static_src/actions/env_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from "../dispatcher"; 2 | import { envActionTypes } from "../constants"; 3 | import cfApi from "../util/cf_api"; 4 | 5 | const fetchEnvSuccess = (appGuid, env) => { 6 | AppDispatcher.handleServerAction({ 7 | type: envActionTypes.ENV_FETCH_ENV_SUCCESS, 8 | appGuid, 9 | env 10 | }); 11 | 12 | return Promise.resolve({ appGuid, env }); 13 | }; 14 | 15 | const fetchEnvFailure = (appGuid, err) => { 16 | AppDispatcher.handleServerAction({ 17 | type: envActionTypes.ENV_FETCH_ENV_FAILURE, 18 | appGuid 19 | }); 20 | 21 | return Promise.resolve({ appGuid, err }); 22 | }; 23 | 24 | export default { 25 | fetchEnv(appGuid) { 26 | AppDispatcher.handleViewAction({ 27 | type: envActionTypes.ENV_FETCH_ENV_REQUEST, 28 | appGuid 29 | }); 30 | 31 | return cfApi 32 | .fetchEnv(appGuid) 33 | .then( 34 | res => fetchEnvSuccess(appGuid, res.data), 35 | err => fetchEnvFailure(appGuid, err) 36 | ); 37 | }, 38 | 39 | invalidateUpdateError(appGuid) { 40 | AppDispatcher.handleViewAction({ 41 | type: envActionTypes.ENV_INVALIDATE_UPDATE_ERROR, 42 | appGuid 43 | }); 44 | 45 | return Promise.resolve(); 46 | }, 47 | 48 | invalidateDeleteError(appGuid) { 49 | return this.invalidateUpdateError(appGuid); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /static_src/actions/error_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for global errors across the whole application. 3 | */ 4 | 5 | import AppDispatcher from "../dispatcher.js"; 6 | import { errorActionTypes } from "../constants"; 7 | 8 | /* eslint-disable no-alert, no-console */ 9 | export default { 10 | errorDelete(err) { 11 | console.error("delete failure", err); 12 | // throw err; 13 | }, 14 | 15 | errorFetch(err) { 16 | console.error("fetch failure", err); 17 | // throw err; 18 | }, 19 | 20 | errorPost(err) { 21 | console.error("post failure", err); 22 | // throw err; 23 | }, 24 | 25 | errorPut(err) { 26 | console.error("put failure", err); 27 | // throw err; 28 | }, 29 | 30 | dismissError(err) { 31 | AppDispatcher.handleUIAction({ 32 | type: errorActionTypes.DISMISS, 33 | err 34 | }); 35 | 36 | return Promise.resolve(err); 37 | }, 38 | 39 | noticeError(err) { 40 | AppDispatcher.handleUIAction({ 41 | type: errorActionTypes.NOTIFY, 42 | err 43 | }); 44 | 45 | return Promise.resolve(err); 46 | }, 47 | 48 | importantDataFetchError(err, entityMessage) { 49 | const msg = 50 | "There was an issue connecting to the dashboard, " + 51 | `${entityMessage || "please try again later."}`; 52 | 53 | AppDispatcher.handleServerAction({ 54 | type: errorActionTypes.IMPORTANT_FETCH, 55 | msg, 56 | err 57 | }); 58 | 59 | return Promise.resolve(err); 60 | }, 61 | 62 | clearErrors() { 63 | AppDispatcher.handleUIAction({ 64 | type: errorActionTypes.CLEAR 65 | }); 66 | 67 | return Promise.resolve(); 68 | } 69 | }; 70 | /* eslint-enable no-alert, no-console */ 71 | -------------------------------------------------------------------------------- /static_src/actions/form_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for app entities. Any actions such as fetching, creating, updating, 3 | * etc should go here. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher.js"; 7 | import { formActionTypes } from "../constants"; 8 | 9 | const formActions = { 10 | changeField(formGuid, fieldName, fieldValue) { 11 | AppDispatcher.handleUIAction({ 12 | type: formActionTypes.FORM_FIELD_CHANGE, 13 | formGuid, 14 | fieldName, 15 | fieldValue 16 | }); 17 | 18 | return Promise.resolve(); 19 | }, 20 | 21 | changeFieldSuccess(formGuid, fieldName) { 22 | AppDispatcher.handleUIAction({ 23 | type: formActionTypes.FORM_FIELD_CHANGE_SUCCESS, 24 | formGuid, 25 | fieldName 26 | }); 27 | 28 | return Promise.resolve(); 29 | }, 30 | 31 | changeFieldError(formGuid, fieldName, error) { 32 | AppDispatcher.handleUIAction({ 33 | type: formActionTypes.FORM_FIELD_CHANGE_ERROR, 34 | formGuid, 35 | fieldName, 36 | error 37 | }); 38 | 39 | return Promise.resolve(); 40 | } 41 | }; 42 | 43 | export default formActions; 44 | -------------------------------------------------------------------------------- /static_src/actions/login_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for login information such as login status. Actions for getting and 3 | * modifying login information should go here. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher.js"; 7 | import cfApi from "../util/cf_api"; 8 | import { loginActionTypes } from "../constants"; 9 | 10 | const loginActions = { 11 | fetchStatus() { 12 | AppDispatcher.handleViewAction({ 13 | type: loginActionTypes.FETCH_STATUS 14 | }); 15 | 16 | return cfApi 17 | .getAuthStatus() 18 | .then(loginActions.receivedStatus) 19 | .catch(loginActions.errorStatus); 20 | }, 21 | 22 | receivedStatus(authStatus) { 23 | AppDispatcher.handleServerAction({ 24 | type: loginActionTypes.RECEIVED_STATUS, 25 | authStatus 26 | }); 27 | 28 | return Promise.resolve(authStatus); 29 | }, 30 | 31 | errorStatus(err) { 32 | AppDispatcher.handleServerAction({ 33 | type: loginActionTypes.ERROR_STATUS, 34 | err 35 | }); 36 | 37 | // Don't return the error since caller is expecting a login status. 38 | // undefined return is important to indicate an error occurred. 39 | return Promise.resolve(); 40 | } 41 | }; 42 | 43 | export default loginActions; 44 | -------------------------------------------------------------------------------- /static_src/actions/page_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from "../dispatcher.js"; 2 | import { pageActionTypes } from "../constants.js"; 3 | 4 | export default { 5 | load() { 6 | AppDispatcher.handleViewAction({ 7 | type: pageActionTypes.PAGE_LOAD_STARTED 8 | }); 9 | 10 | return Promise.resolve(); 11 | }, 12 | 13 | loadSuccess() { 14 | AppDispatcher.handleViewAction({ 15 | type: pageActionTypes.PAGE_LOAD_SUCCESS 16 | }); 17 | 18 | return Promise.resolve(); 19 | }, 20 | 21 | loadError() { 22 | AppDispatcher.handleViewAction({ 23 | type: pageActionTypes.PAGE_LOAD_ERROR 24 | }); 25 | 26 | return Promise.resolve(); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /static_src/actions/quota_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for route entities. Any actions such as fetching, creating, updating, 3 | * etc should go here. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher.js"; 7 | import { quotaActionTypes } from "../constants"; 8 | 9 | export default { 10 | fetchAll() { 11 | this.fetchQuotasForAllOrgs(); 12 | this.fetchQuotasForAllSpaces(); 13 | }, 14 | 15 | fetchQuotasForAllOrgs() { 16 | AppDispatcher.handleViewAction({ 17 | type: quotaActionTypes.ORGS_QUOTAS_FETCH 18 | }); 19 | }, 20 | 21 | receivedQuotasForAllOrgs(quotas) { 22 | AppDispatcher.handleServerAction({ 23 | type: quotaActionTypes.ORGS_QUOTAS_RECEIVED, 24 | quotas 25 | }); 26 | }, 27 | 28 | fetchQuotasForAllSpaces() { 29 | AppDispatcher.handleViewAction({ 30 | type: quotaActionTypes.SPACES_QUOTAS_FETCH 31 | }); 32 | }, 33 | 34 | receivedQuotasForAllSpaces(quotas) { 35 | AppDispatcher.handleServerAction({ 36 | type: quotaActionTypes.SPACES_QUOTAS_RECEIVED, 37 | quotas 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /static_src/actions/router_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from "../dispatcher.js"; 2 | import { routerActionTypes } from "../constants"; 3 | 4 | const routerActions = { 5 | navigate(component, props) { 6 | AppDispatcher.handleViewAction({ 7 | type: routerActionTypes.NAVIGATE, 8 | data: { 9 | component, 10 | props 11 | } 12 | }); 13 | } 14 | }; 15 | 16 | export default routerActions; 17 | -------------------------------------------------------------------------------- /static_src/actions/space_actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Actions for space entities. Any actions such as fetching, creating, updating, 3 | * etc should go here. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher.js"; 7 | import cfApi from "../util/cf_api"; 8 | import errorActions from "./error_actions.js"; 9 | import { spaceActionTypes } from "../constants.js"; 10 | import SpaceStore from "../stores/space_store"; 11 | 12 | export default { 13 | fetch(spaceGuid) { 14 | AppDispatcher.handleViewAction({ 15 | type: spaceActionTypes.SPACE_FETCH, 16 | spaceGuid 17 | }); 18 | 19 | return cfApi 20 | .fetchSpace(spaceGuid) 21 | .then(this.receivedSpace) 22 | .catch(err => 23 | errorActions.importantDataFetchError(err, "unable to fetch space") 24 | ); 25 | }, 26 | 27 | fetchAll() { 28 | AppDispatcher.handleViewAction({ 29 | type: spaceActionTypes.SPACES_FETCH 30 | }); 31 | 32 | return cfApi 33 | .fetchSpaces() 34 | .then(this.receivedSpaces) 35 | .catch(err => 36 | errorActions.importantDataFetchError( 37 | err, 38 | "space data may be incomplete" 39 | ) 40 | ); 41 | }, 42 | 43 | fetchAllForOrg(orgGuid) { 44 | AppDispatcher.handleViewAction({ 45 | type: spaceActionTypes.SPACES_FOR_ORG_FETCH, 46 | orgGuid 47 | }); 48 | 49 | return Promise.all( 50 | SpaceStore.getAll() 51 | .filter(space => space.organization_guid === orgGuid) 52 | .map(space => this.fetch(space.guid)) 53 | ).catch(err => 54 | errorActions.importantDataFetchError(err, "space data may be incomplete") 55 | ); 56 | }, 57 | 58 | receivedSpace(space) { 59 | AppDispatcher.handleServerAction({ 60 | type: spaceActionTypes.SPACE_RECEIVED, 61 | space 62 | }); 63 | 64 | return Promise.resolve(space); 65 | }, 66 | 67 | receivedSpaces(spaces) { 68 | AppDispatcher.handleServerAction({ 69 | type: spaceActionTypes.SPACES_RECEIVED, 70 | spaces 71 | }); 72 | 73 | return Promise.resolve(spaces); 74 | }, 75 | 76 | changeCurrentSpace(spaceGuid) { 77 | AppDispatcher.handleUIAction({ 78 | type: spaceActionTypes.SPACE_CHANGE_CURRENT, 79 | spaceGuid 80 | }); 81 | 82 | return Promise.resolve(spaceGuid || null); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /static_src/actions/upsi_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from "../dispatcher"; 2 | import { upsiActionTypes } from "../constants"; 3 | import cfApi from "../util/cf_api"; 4 | 5 | export const fetchAllSuccess = items => { 6 | AppDispatcher.handleServerAction({ 7 | type: upsiActionTypes.UPSI_FETCH_ALL_SUCCESS, 8 | items 9 | }); 10 | 11 | return Promise.resolve({ items }); 12 | }; 13 | 14 | export const fetchAllFailure = err => { 15 | AppDispatcher.handleServerAction({ 16 | type: upsiActionTypes.UPSI_FETCH_ALL_FAILURE 17 | }); 18 | 19 | return Promise.resolve({ err }); 20 | }; 21 | 22 | export const fetchAllForSpaceSuccess = (spaceGuid, items) => { 23 | AppDispatcher.handleServerAction({ 24 | type: upsiActionTypes.UPSI_FETCH_ALL_FOR_SPACE_SUCCESS, 25 | spaceGuid, 26 | items 27 | }); 28 | 29 | return Promise.resolve({ spaceGuid, items }); 30 | }; 31 | 32 | export const fetchAllForSpaceFailure = (spaceGuid, err) => { 33 | AppDispatcher.handleServerAction({ 34 | type: upsiActionTypes.UPSI_FETCH_ALL_FOR_SPACE_FAILURE, 35 | spaceGuid 36 | }); 37 | 38 | return Promise.resolve({ spaceGuid, err }); 39 | }; 40 | 41 | export default { 42 | fetchAll() { 43 | AppDispatcher.handleViewAction({ 44 | type: upsiActionTypes.UPSI_FETCH_ALL_REQUEST 45 | }); 46 | 47 | return cfApi 48 | .fetchAllUPSI() 49 | .then(data => fetchAllSuccess(data), err => fetchAllFailure(err)); 50 | }, 51 | 52 | fetchAllForSpace(spaceGuid) { 53 | AppDispatcher.handleViewAction({ 54 | type: upsiActionTypes.UPSI_FETCH_ALL_FOR_SPACE_REQUEST, 55 | spaceGuid 56 | }); 57 | 58 | return cfApi 59 | .fetchAllUPSI({ 60 | q: [{ filter: "space_guid", op: ":", value: spaceGuid }] 61 | }) 62 | .then( 63 | data => fetchAllForSpaceSuccess(spaceGuid, data), 64 | err => fetchAllForSpaceFailure(err) 65 | ); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /static_src/components/action/button.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | children: PropTypes.any, 6 | className: PropTypes.string, 7 | clickHandler: PropTypes.func, 8 | disabled: PropTypes.bool, 9 | label: PropTypes.string, 10 | type: PropTypes.string 11 | }; 12 | 13 | const button = ({ 14 | className, 15 | label, 16 | clickHandler, 17 | disabled, 18 | type, 19 | children 20 | }) => ( 21 | 30 | ); 31 | 32 | button.propTypes = propTypes; 33 | 34 | export default button; 35 | -------------------------------------------------------------------------------- /static_src/components/action/link.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classnames from "classnames"; 4 | 5 | const propTypes = { 6 | children: PropTypes.any, 7 | className: PropTypes.string, 8 | clickHandler: PropTypes.func, 9 | href: PropTypes.string, 10 | label: PropTypes.string 11 | }; 12 | const defaultHref = "#"; 13 | 14 | const Link = ({ className, label, href, clickHandler, children }) => ( 15 | 21 | {children} 22 | 23 | ); 24 | 25 | Link.propTypes = propTypes; 26 | 27 | export default Link; 28 | -------------------------------------------------------------------------------- /static_src/components/app_activity/crash_event_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const crashReason = (status, description) => { 5 | switch (description) { 6 | case "app instance exited": 7 | return `the app instance exited with ${status} status`; 8 | case "out of memory": 9 | return "it ran out of memory"; 10 | case "failed to accept connections within health check timeout": 11 | case "failed to start": 12 | return `it ${description}`; 13 | default: 14 | return "of an unknown reason"; 15 | } 16 | }; 17 | 18 | const propTypes = { 19 | exitDescription: PropTypes.string, 20 | exitStatus: PropTypes.string 21 | }; 22 | 23 | const CrashEventItem = ({ exitStatus, exitDescription }) => ( 24 | 25 | The app crashed because {crashReason(exitStatus, exitDescription)}. 26 | 27 | ); 28 | 29 | CrashEventItem.propTypes = propTypes; 30 | export default CrashEventItem; 31 | -------------------------------------------------------------------------------- /static_src/components/app_activity/log_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | statusCode: PropTypes.string, 6 | requestedUrl: PropTypes.string 7 | }; 8 | 9 | const LogItem = ({ statusCode, requestedUrl }) => ( 10 | 11 | {statusCode} {requestedUrl} 12 | 13 | ); 14 | 15 | LogItem.propTypes = propTypes; 16 | 17 | export default LogItem; 18 | -------------------------------------------------------------------------------- /static_src/components/app_activity/raw_json_detail.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | item: PropTypes.object, 6 | visible: PropTypes.bool 7 | }; 8 | 9 | const RawJSONDetail = ({ item, visible }) => { 10 | if (!visible) return null; 11 | 12 | return ( 13 |
14 |
Raw event log
15 | 16 |
{JSON.stringify(item, null, 2)}
17 |
18 |
19 | ); 20 | }; 21 | 22 | RawJSONDetail.propTypes = propTypes; 23 | export default RawJSONDetail; 24 | -------------------------------------------------------------------------------- /static_src/components/app_activity/route_event_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import formatRoute from "../../util/format_route"; 4 | 5 | const urlPlaceholder = "url"; 6 | const propTypes = { 7 | actor: PropTypes.string, 8 | domain: PropTypes.shape({ name: PropTypes.string }), 9 | route: PropTypes.shape({ host: PropTypes.string, path: PropTypes.string }), 10 | unmapped: PropTypes.bool 11 | }; 12 | 13 | const appRouteLink = (domain, route) => { 14 | if (!route || !domain) { 15 | return urlPlaceholder; 16 | } 17 | 18 | const href = formatRoute(domain.name, route.host, route.path); 19 | 20 | return {href}; 21 | }; 22 | 23 | const RouteEventItem = ({ actor, domain, route, unmapped }) => ( 24 | 25 | {actor} {unmapped || "mapped"} {appRouteLink(domain, route)} to the app. 26 | 27 | ); 28 | 29 | RouteEventItem.propTypes = propTypes; 30 | export default RouteEventItem; 31 | -------------------------------------------------------------------------------- /static_src/components/app_activity/timestamp.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import moment from "moment-timezone"; 4 | 5 | const propTypes = { timestamp: PropTypes.string.isRequired }; 6 | const formatTimestamp = timestamp => 7 | moment(timestamp) 8 | .tz(moment.tz.guess()) 9 | .format("MMM DD YYYY HH:mm:ss z"); 10 | 11 | const Timestamp = ({ timestamp }) => ( 12 | 13 | {formatTimestamp(timestamp)} 14 | 15 | ); 16 | 17 | Timestamp.propTypes = propTypes; 18 | 19 | export default Timestamp; 20 | -------------------------------------------------------------------------------- /static_src/components/app_count_status.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import CountStatus from "./count_status.jsx"; 5 | import { entityHealth } from "../constants.js"; 6 | import { appHealth, worstHealth } from "../util/health"; 7 | 8 | const propTypes = { 9 | appCount: PropTypes.number, 10 | apps: PropTypes.array 11 | }; 12 | 13 | const defaultProps = { 14 | appCount: 0, 15 | apps: [] 16 | }; 17 | 18 | export default class AppCountStatus extends React.Component { 19 | render() { 20 | const props = this.props; 21 | let health = entityHealth.inactive; 22 | 23 | if (props.apps.length) { 24 | health = worstHealth(props.apps.map(appHealth)); 25 | } 26 | 27 | return ( 28 | 34 | ); 35 | } 36 | } 37 | 38 | AppCountStatus.propTypes = propTypes; 39 | AppCountStatus.defaultProps = defaultProps; 40 | -------------------------------------------------------------------------------- /static_src/components/breadcrumbs/breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { appPropType } from "../../stores/app_store"; 4 | import { orgPropType } from "../../stores/org_store"; 5 | import { spacePropType } from "../../stores/space_store"; 6 | import BreadcrumbsItem from "./breadcrumbs_item"; 7 | import { orgHref, spaceHref } from "../../util/url"; 8 | 9 | const propTypes = { 10 | org: orgPropType.isRequired, 11 | space: spacePropType, 12 | app: appPropType 13 | }; 14 | 15 | const Breadcrumbs = ({ org, space, app }) => { 16 | const items = [ 17 | 18 | Overview 19 | 20 | ]; 21 | 22 | if (org && space) { 23 | items.push( 24 | 25 | {org.name} 26 | 27 | ); 28 | } 29 | 30 | if (org && space && app) { 31 | items.push( 32 | 37 | {space.name} 38 | 39 | ); 40 | } 41 | 42 | return ( 43 |
    44 | {items} 45 |
46 | ); 47 | }; 48 | 49 | Breadcrumbs.propTypes = propTypes; 50 | 51 | export default Breadcrumbs; 52 | -------------------------------------------------------------------------------- /static_src/components/breadcrumbs/breadcrumbs_item.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | children: PropTypes.node.isRequired, 6 | href: PropTypes.string, 7 | testLabel: PropTypes.string 8 | }; 9 | 10 | const BreadcrumbsItem = ({ children, href, testLabel }) => ( 11 |
  • 12 | {href ? ( 13 | 14 | {children} 15 | 16 | ) : ( 17 | 18 | {children} 19 | 20 | )} 21 |
  • 22 | ); 23 | 24 | BreadcrumbsItem.propTypes = propTypes; 25 | 26 | export default BreadcrumbsItem; 27 | -------------------------------------------------------------------------------- /static_src/components/breadcrumbs/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./breadcrumbs"; 2 | -------------------------------------------------------------------------------- /static_src/components/complex_list.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | import ComplexListItem from "./complex_list_item.jsx"; 5 | 6 | const propTypes = { 7 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), 8 | className: PropTypes.string, 9 | title: PropTypes.string, 10 | titleElement: PropTypes.element, 11 | emptyMessage: PropTypes.element 12 | }; 13 | const defaultProps = { 14 | children: [], 15 | title: null, 16 | titleElement: null, 17 | emptyMessage: null 18 | }; 19 | 20 | export default class ComplexList extends React.Component { 21 | hasAnyTitle() { 22 | return !!(this.props.title || this.props.titleElement); 23 | } 24 | 25 | render() { 26 | const emptyMessage = this.props.emptyMessage && ( 27 |
    {this.props.emptyMessage}
    28 | ); 29 | let header; 30 | 31 | if (this.hasAnyTitle()) { 32 | let title; 33 | if (this.props.titleElement) { 34 | title = this.props.titleElement; 35 | } else { 36 | title = this.props.title; 37 | } 38 | header = ( 39 |
    40 |

    {title}

    41 |
    42 | ); 43 | } 44 | 45 | const classes = classNames("complex_list", { 46 | [this.props.className]: this.props.className 47 | }); 48 | 49 | return ( 50 |
    51 | {header} 52 | {emptyMessage} 53 | {this.props.children.length > 0 && 54 | this.props.children.map((child, i) => { 55 | if (child.type === ComplexList) { 56 | return child; 57 | } 58 | return {child}; 59 | })} 60 |
    61 | ); 62 | } 63 | } 64 | 65 | ComplexList.propTypes = propTypes; 66 | ComplexList.defaultProps = defaultProps; 67 | -------------------------------------------------------------------------------- /static_src/components/complex_list_item.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | children: PropTypes.any 6 | }; 7 | 8 | const defaultProps = {}; 9 | 10 | export default class ComplexListItem extends React.Component { 11 | render() { 12 | return
    {this.props.children}
    ; 13 | } 14 | } 15 | 16 | ComplexListItem.propTypes = propTypes; 17 | ComplexListItem.defaultProps = defaultProps; 18 | -------------------------------------------------------------------------------- /static_src/components/confirmation_box.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * A component that renders a box with a different style and background 3 | */ 4 | import PropTypes from "prop-types"; 5 | import React from "react"; 6 | import Action from "./action.jsx"; 7 | 8 | const CONFIRM_STYLES = { 9 | INLINE: "inline", 10 | NEXTO: "nexto", 11 | OVER: "over", 12 | BLOCK: "block" 13 | }; 14 | 15 | const propTypes = { 16 | style: PropTypes.oneOf( 17 | Object.keys(CONFIRM_STYLES).map(key => CONFIRM_STYLES[key]) 18 | ), 19 | message: PropTypes.any, 20 | confirmationText: PropTypes.string, 21 | confirmHandler: PropTypes.func.isRequired, 22 | cancelHandler: PropTypes.func.isRequired, 23 | disabled: PropTypes.bool 24 | }; 25 | 26 | const defaultProps = { 27 | style: CONFIRM_STYLES.INLINE, 28 | message:
    , 29 | confirmationText: "Confirm delete", 30 | confirmHandler: ev => { 31 | console.log("confirm ev", ev); 32 | }, 33 | cancelHandler: ev => { 34 | console.log("cancel ev", ev); 35 | } 36 | }; 37 | 38 | export default class ConfirmationBox extends React.Component { 39 | render() { 40 | const styleClass = `confirm-${this.props.style}`; 41 | 42 | return ( 43 |
    44 | {this.props.message} 45 |
    46 | 53 | Cancel 54 | 55 | 61 | {this.props.confirmationText} 62 | 63 |
    64 |
    65 | ); 66 | } 67 | } 68 | 69 | ConfirmationBox.propTypes = propTypes; 70 | 71 | ConfirmationBox.defaultProps = defaultProps; 72 | -------------------------------------------------------------------------------- /static_src/components/count_status.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import { entityHealth } from "../constants.js"; 4 | import EntityIcon from "./entity_icon.jsx"; 5 | 6 | const ICON_TYPES = ["space", "app", "service"]; 7 | 8 | const propTypes = { 9 | count: PropTypes.number, 10 | name: PropTypes.string.isRequired, 11 | health: PropTypes.oneOf(Object.values(entityHealth)), 12 | iconType: PropTypes.oneOf(ICON_TYPES) 13 | }; 14 | 15 | const defaultProps = { 16 | count: 0, 17 | health: entityHealth.inactive, 18 | iconType: "space" 19 | }; 20 | 21 | export default class CountStatus extends React.Component { 22 | render() { 23 | const { health, iconType, count, name } = this.props; 24 | 25 | return ( 26 |
    27 |
    28 | 29 |
    30 |
    31 | {count} {name} 32 |
    33 |
    34 | ); 35 | } 36 | } 37 | 38 | CountStatus.propTypes = propTypes; 39 | CountStatus.defaultProps = defaultProps; 40 | -------------------------------------------------------------------------------- /static_src/components/dropdown.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import style from "cloudgov-style/css/cloudgov-style.css"; 5 | 6 | import classNames from "classnames"; 7 | 8 | const propTypes = { 9 | title: PropTypes.string.isRequired, 10 | items: PropTypes.any, 11 | classes: PropTypes.array 12 | }; 13 | 14 | export default class Dropdown extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { open: false }; 19 | this.handleTitleClick = this.handleTitleClick.bind(this); 20 | } 21 | 22 | handleTitleClick(ev) { 23 | this.setState({ open: !this.state.open }); 24 | } 25 | 26 | render() { 27 | var id = "dropdown-" + this.props.title, 28 | classes = classNames(style.dropdown, this.props.classes, { 29 | open: !!this.state.open 30 | }); 31 | 32 | return ( 33 |
    34 | 41 | {this.props.title} 42 | 43 |
      44 | {this.props.items.map(item => { 45 | return
    • {item.element}
    • ; 46 | })} 47 |
    48 |
    49 | ); 50 | } 51 | } 52 | 53 | Dropdown.propTypes = propTypes; 54 | -------------------------------------------------------------------------------- /static_src/components/elastic_line.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | children: PropTypes.array 6 | }; 7 | const defaultProps = { 8 | children: null 9 | }; 10 | 11 | export default class ElasticLine extends React.Component { 12 | render() { 13 | return
    {this.props.children}
    ; 14 | } 15 | } 16 | 17 | ElasticLine.propTypes = propTypes; 18 | ElasticLine.defaultProps = defaultProps; 19 | -------------------------------------------------------------------------------- /static_src/components/elastic_line_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | const alignStyles = ["start", "end"]; 6 | 7 | const propTypes = { 8 | children: PropTypes.any, 9 | align: PropTypes.oneOf(alignStyles), 10 | title: PropTypes.string.isRequired 11 | }; 12 | 13 | const defaultProps = { 14 | align: alignStyles[0], 15 | title: "" 16 | }; 17 | 18 | const ElasticLineItem = ({ children, align, title }) => ( 19 |
    25 | {children} 26 |
    27 | ); 28 | 29 | ElasticLineItem.propTypes = propTypes; 30 | 31 | ElasticLineItem.defaultProps = defaultProps; 32 | 33 | export default ElasticLineItem; 34 | -------------------------------------------------------------------------------- /static_src/components/entity_empty.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import PanelDocumentation from "./panel_documentation.jsx"; 4 | 5 | const propTypes = { 6 | callout: PropTypes.node, 7 | children: PropTypes.any 8 | }; 9 | 10 | const defaultProps = { 11 | children: null 12 | }; 13 | 14 | export default class EntityEmpty extends React.Component { 15 | render() { 16 | const props = this.props; 17 | return ( 18 | 19 |
    20 |

    {props.callout}

    21 | {props.children} 22 |
    23 |
    24 | ); 25 | } 26 | } 27 | 28 | EntityEmpty.propTypes = propTypes; 29 | EntityEmpty.defaultProps = defaultProps; 30 | -------------------------------------------------------------------------------- /static_src/components/entity_icon.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import Icon from "./icon.jsx"; 5 | import { entityHealth } from "../constants.js"; 6 | 7 | const ENTITIES = ["app", "service", "space", "org"]; 8 | 9 | const propTypes = { 10 | children: PropTypes.node, 11 | entity: PropTypes.oneOf(ENTITIES).isRequired, 12 | iconSize: PropTypes.string, 13 | health: PropTypes.oneOf(Object.values(entityHealth)) 14 | }; 15 | 16 | const defaultProps = { 17 | health: entityHealth.default 18 | }; 19 | 20 | export default class EntityIcon extends React.Component { 21 | render() { 22 | const statusClass = this.props.health; 23 | 24 | return ( 25 | 32 | {this.props.children} 33 | 34 | ); 35 | } 36 | } 37 | 38 | EntityIcon.propTypes = propTypes; 39 | EntityIcon.defaultProps = defaultProps; 40 | -------------------------------------------------------------------------------- /static_src/components/env/env_panel/header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | children: PropTypes.any.isRequired 6 | }; 7 | 8 | const Header = ({ children }) => ( 9 |
    10 |

    {children}

    11 |
    12 | ); 13 | 14 | Header.propTypes = propTypes; 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /static_src/components/env/env_panel/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./env_panel"; 2 | -------------------------------------------------------------------------------- /static_src/components/env/env_panel/section.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | children: PropTypes.any.isRequired 6 | }; 7 | 8 | const Section = ({ children }) =>
    {children}
    ; 9 | 10 | Section.propTypes = propTypes; 11 | 12 | export default Section; 13 | -------------------------------------------------------------------------------- /static_src/components/error_message.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | const propTypes = { 6 | error: PropTypes.object, 7 | inline: PropTypes.bool.isRequired 8 | }; 9 | 10 | const defaultProps = { 11 | error: null, 12 | inline: false 13 | }; 14 | 15 | export default class ErrorMessage extends Component { 16 | renderMessage() { 17 | const { error } = this.props; 18 | if (!error) { 19 | return "An error occurred"; 20 | } 21 | const { message, description } = error; 22 | return message || description; 23 | } 24 | 25 | render() { 26 | const { error, inline } = this.props; 27 | 28 | if (!error) { 29 | return null; 30 | } 31 | 32 | return ( 33 |
    39 | {this.renderMessage()} 40 |
    41 | ); 42 | } 43 | } 44 | 45 | ErrorMessage.propTypes = propTypes; 46 | 47 | ErrorMessage.defaultProps = defaultProps; 48 | -------------------------------------------------------------------------------- /static_src/components/expandable_box.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | 5 | const propTypes = { 6 | children: PropTypes.any, 7 | classes: PropTypes.array, 8 | clickHandler: PropTypes.func, 9 | clickableContent: PropTypes.any, 10 | isExpanded: PropTypes.bool 11 | }; 12 | const defaultProps = { 13 | children: null, 14 | classes: [], 15 | clickHandler: () => {}, 16 | clickableContent: null, 17 | isExpanded: false 18 | }; 19 | 20 | export default class ExpandableBox extends React.Component { 21 | render() { 22 | const classes = classNames("expandable_box", { 23 | "expandable_box-is_expanded": this.props.isExpanded, 24 | [`${this.props.classes.join("")}`]: this.props.classes.length 25 | }); 26 | 27 | return ( 28 |
    29 |
    30 | {this.props.clickableContent} 31 |
    32 | {this.props.children} 33 |
    34 | ); 35 | } 36 | } 37 | 38 | ExpandableBox.propTypes = propTypes; 39 | ExpandableBox.defaultProps = defaultProps; 40 | -------------------------------------------------------------------------------- /static_src/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { config } from "skin"; 4 | 5 | export default class Header extends React.Component { 6 | render() { 7 | return ( 8 |
    9 |
    10 |
      11 | {config.footer.links.map(link => ( 12 |
    • 13 | {link.text} 14 |
    • 15 | ))} 16 |
    17 |
    18 |
    19 |
      20 |
    • {config.footer.author_note}
    • 21 |
    • {config.footer.code_note}
    • 22 |
    • {config.footer.disclaimer_note}
    • 23 |
    24 |
    25 |
    26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /static_src/components/form/form_error.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { message: PropTypes.string }; 5 | const defaultProps = { message: "" }; 6 | 7 | const FormError = ({ message }) => ( 8 | {message} 9 | ); 10 | 11 | FormError.propTypes = propTypes; 12 | FormError.defaultProps = defaultProps; 13 | 14 | export default FormError; 15 | -------------------------------------------------------------------------------- /static_src/components/form/form_number.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import FormText from "./form_text.jsx"; 5 | import { validateInteger } from "../../util/validators"; 6 | 7 | const propTypes = { 8 | min: PropTypes.number, 9 | max: PropTypes.number 10 | }; 11 | 12 | export default class FormNumber extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.validateInteger = validateInteger({ 16 | max: props.max, 17 | min: props.min 18 | }).bind(this); 19 | } 20 | 21 | componentWillReceiveProps(props) { 22 | this.validateInteger = validateInteger({ 23 | max: props.max, 24 | min: props.min 25 | }).bind(this); 26 | } 27 | 28 | render() { 29 | const props = Object.assign({}, this.props, { 30 | validator: this.validateInteger 31 | }); 32 | return ; 33 | } 34 | } 35 | 36 | FormNumber.propTypes = propTypes; 37 | -------------------------------------------------------------------------------- /static_src/components/form/form_select.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import FormElement from "./form_element.jsx"; 5 | import FormError from "./form_error.jsx"; 6 | 7 | export default class FormSelect extends FormElement { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = this.state || {}; 12 | this.state.value = ""; 13 | this.state.err = null; 14 | } 15 | 16 | render() { 17 | let error; 18 | 19 | if (this.state.err) { 20 | error = ; 21 | } 22 | return ( 23 |
    24 | {error} 25 | 26 | 42 |
    43 | ); 44 | } 45 | } 46 | FormSelect.propTypes = Object.assign(FormSelect.propTypes, { 47 | options: PropTypes.array 48 | }); 49 | FormSelect.defaultProps = Object.assign(FormSelect.defaultProps, { 50 | options: [] 51 | }); 52 | -------------------------------------------------------------------------------- /static_src/components/form/form_text.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import FormElement from "./form_element.jsx"; 4 | import FormError from "./form_error.jsx"; 5 | import classNames from "classnames"; 6 | 7 | export default class FormText extends FormElement { 8 | get error() { 9 | if (!this.state.err) { 10 | return null; 11 | } 12 | 13 | return ; 14 | } 15 | 16 | render() { 17 | const classes = classNames({ 18 | "form_text-inline": this.props.inline, 19 | error: !!this.error 20 | }); 21 | 22 | // Spaces in label give a healthy space for inline forms 23 | const label = ; 24 | return ( 25 |
    26 | {!this.props.labelAfter && label} 27 | 34 | {this.props.labelAfter && label} 35 | {this.error} 36 |
    37 | ); 38 | } 39 | } 40 | 41 | FormText.propTypes = Object.assign({}, FormElement.propTypes, { 42 | inline: PropTypes.bool, 43 | labelAfter: PropTypes.bool 44 | }); 45 | 46 | FormText.defaultProps = Object.assign({}, FormElement.defaultProps, { 47 | inline: false, 48 | labelAfter: false 49 | }); 50 | -------------------------------------------------------------------------------- /static_src/components/form/index.js: -------------------------------------------------------------------------------- 1 | import Form from "./form.jsx"; 2 | import FormElement from "./form_element.jsx"; 3 | import FormError from "./form_error.jsx"; 4 | import FormNumber from "./form_number.jsx"; 5 | import FormSelect from "./form_select.jsx"; 6 | import FormText from "./form_text.jsx"; 7 | 8 | export { Form, FormElement, FormError, FormNumber, FormSelect, FormText }; 9 | -------------------------------------------------------------------------------- /static_src/components/global_error.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import Notification from "./notification.jsx"; 5 | import { config } from "skin"; 6 | import errorActions from "../actions/error_actions.js"; 7 | 8 | const propTypes = { 9 | err: PropTypes.object 10 | }; 11 | 12 | const defaultProps = {}; 13 | 14 | export default class GlobalError extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.onNotificationDismiss = this.onNotificationDismiss.bind(this); 19 | this.onNotificationRefresh = this.onNotificationRefresh.bind(this); 20 | } 21 | 22 | onNotificationDismiss(ev) { 23 | ev.preventDefault(); 24 | errorActions.dismissError(this.props.err); 25 | } 26 | 27 | onNotificationRefresh(ev) { 28 | ev.preventDefault(); 29 | window.location.reload(); 30 | } 31 | 32 | render() { 33 | const err = this.props.err; 34 | const link = config.docs.status && ( 35 | 36 | {" "} 37 | check 38 | {config.platform.name}'s status 39 | {" "} 40 | or 41 | 42 | ); 43 | 44 | const description = err.description || "An unknown error occurred"; 45 | const wrappedDescription = ( 46 | 47 | {description}. {description.length > 80 &&
    } 48 | Please {link} try again. 49 |
    50 | ); 51 | 52 | return ( 53 | 60 | ); 61 | } 62 | } 63 | 64 | GlobalError.propTypes = propTypes; 65 | GlobalError.defaultProps = defaultProps; 66 | -------------------------------------------------------------------------------- /static_src/components/global_error_container.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | import ErrorStore from "../stores/error_store.js"; 5 | import GlobalError from "./global_error.jsx"; 6 | 7 | const propTypes = { 8 | maxItems: PropTypes.number 9 | }; 10 | const defaultProps = { 11 | maxItems: 1 12 | }; 13 | 14 | function stateSetter() { 15 | const errs = ErrorStore.getAll(); 16 | 17 | return { 18 | errs 19 | }; 20 | } 21 | 22 | export default class GlobalErrorContainer extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = stateSetter(); 27 | 28 | this.handleChange = this.handleChange.bind(this); 29 | } 30 | 31 | componentDidMount() { 32 | ErrorStore.addChangeListener(this.handleChange); 33 | } 34 | 35 | componentWillUnmount() { 36 | ErrorStore.removeChangeListener(this.handleChange); 37 | } 38 | 39 | handleChange() { 40 | this.setState(stateSetter()); 41 | } 42 | 43 | render() { 44 | let errNotifications; 45 | 46 | if (this.state.errs.length) { 47 | errNotifications = []; 48 | this.state.errs.slice(0, this.props.maxItems).forEach((err, i) => { 49 | const errorMessage = ; 50 | errNotifications.push(errorMessage); 51 | }); 52 | } 53 | 54 | return
    {errNotifications}
    ; 55 | } 56 | } 57 | 58 | GlobalErrorContainer.propTypes = propTypes; 59 | GlobalErrorContainer.defaultProps = defaultProps; 60 | -------------------------------------------------------------------------------- /static_src/components/header/header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | 4 | import { header } from "skin"; 5 | import LoginStore from "../../stores/login_store.js"; 6 | import HeaderLink from "./header_link"; 7 | import Action from "../action.jsx"; 8 | 9 | const Header = () => { 10 | const loginLink = LoginStore.isLoggedIn() ? ( 11 | 12 | 13 | Log out 14 | 15 | 16 | ) : ( 17 | 18 | 19 | Login 20 | 21 | 22 | ); 23 | 24 | return ( 25 |
    26 |
    27 |
    28 | {header.logo.render()} 29 | 37 |
    38 | 45 |
    46 |
    47 | ); 48 | }; 49 | 50 | export default Header; 51 | -------------------------------------------------------------------------------- /static_src/components/header/header_link.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | const propTypes = { 6 | children: PropTypes.any, 7 | url: PropTypes.string, 8 | text: PropTypes.string, 9 | classes: PropTypes.array.isRequired 10 | }; 11 | 12 | const defaultProps = { 13 | classes: [] 14 | }; 15 | 16 | const HeaderLink = ({ children, url, text, classes }) => ( 17 |
  • 18 | {children || ( 19 | 20 | {text} 21 | 22 | )} 23 |
  • 24 | ); 25 | 26 | HeaderLink.propTypes = propTypes; 27 | 28 | HeaderLink.defaultProps = defaultProps; 29 | 30 | export default HeaderLink; 31 | -------------------------------------------------------------------------------- /static_src/components/header/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./header"; 2 | -------------------------------------------------------------------------------- /static_src/components/icon.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | import iconImg from "cloudgov-style/img/cloudgov-sprite.svg"; 5 | 6 | const ICON_TYPES = ["fill", "stroke"]; 7 | 8 | const ICON_SIZE = ["small", "medium", "large"]; 9 | 10 | const STYLE_TYPES = ["alt", "ok", "inactive", "error", "default"]; 11 | 12 | const propTypes = { 13 | children: PropTypes.node, 14 | name: PropTypes.string.isRequired, 15 | styleType: PropTypes.oneOf(STYLE_TYPES), 16 | iconType: PropTypes.oneOf(ICON_TYPES), 17 | iconSize: PropTypes.oneOf(ICON_SIZE), 18 | bordered: PropTypes.bool 19 | }; 20 | 21 | const defaultProps = { 22 | styleType: "default", 23 | iconType: "stroke", 24 | bordered: false 25 | }; 26 | 27 | export default class Icon extends React.Component { 28 | getImagePath(iconName) { 29 | const fill = this.props.iconType === "fill" ? "fill-" : ""; 30 | return `assets/${iconImg}#i-${fill}${iconName}`; 31 | } 32 | 33 | render() { 34 | const classes = classNames(`icon-${this.props.styleType}`, { 35 | [`icon-${this.props.iconSize}`]: this.props.iconSize, 36 | "icon-bordered": this.props.bordered, 37 | icon: !this.props.iconType, 38 | "icon-fill": this.props.iconType 39 | }); 40 | 41 | return ( 42 | 43 | 44 | 45 | {" "} 46 | {this.props.children} 47 | 48 | ); 49 | } 50 | } 51 | 52 | Icon.propTypes = propTypes; 53 | Icon.defaultProps = defaultProps; 54 | -------------------------------------------------------------------------------- /static_src/components/info_logs.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { config } from "skin"; 4 | import PanelActions from "./panel_actions.jsx"; 5 | 6 | export default class InfoLogs extends React.Component { 7 | render() { 8 | return ( 9 | 10 |

    11 | View more logs at{" "} 12 | {config.platform.logs.name}. 13 |

    14 |
    15 | ); 16 | } 17 | } 18 | 19 | InfoLogs.propTypes = {}; 20 | InfoLogs.defaultProps = {}; 21 | -------------------------------------------------------------------------------- /static_src/components/main_container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import userProvider from "./user_provider.jsx"; 4 | import Disclaimer from "./header/disclaimer"; 5 | import Footer from "./footer.jsx"; 6 | import GlobalErrorContainer from "./global_error_container.jsx"; 7 | import Header from "./header"; 8 | import LoginStore from "../stores/login_store.js"; 9 | import OrgStore from "../stores/org_store.js"; 10 | import SpaceStore from "../stores/space_store.js"; 11 | 12 | const propTypes = { 13 | children: PropTypes.any 14 | }; 15 | 16 | function stateSetter() { 17 | return { 18 | currentOrgGuid: OrgStore.currentOrgGuid, 19 | currentSpaceGuid: SpaceStore.currentSpaceGuid, 20 | isLoggedIn: LoginStore.isLoggedIn() 21 | }; 22 | } 23 | 24 | class App extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = stateSetter(); 29 | this.handleChange = this.handleChange.bind(this); 30 | } 31 | 32 | componentDidMount() { 33 | LoginStore.addChangeListener(this.handleChange); 34 | } 35 | 36 | componentWillUnmount() { 37 | LoginStore.removeChangeListener(this.handleChange); 38 | } 39 | 40 | handleChange() { 41 | this.setState(stateSetter()); 42 | } 43 | 44 | render() { 45 | return ( 46 |
    47 | 48 |
    49 |
    50 | 51 |
    52 |
    {this.props.children}
    53 |
    54 |
    55 |
    56 |
    57 | ); 58 | } 59 | } 60 | 61 | App.propTypes = propTypes; 62 | 63 | App.defaultProps = { 64 | children: [] 65 | }; 66 | 67 | export default userProvider(App); 68 | -------------------------------------------------------------------------------- /static_src/components/not_found.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () =>

    Error: Not Found

    ; 4 | 5 | export default NotFound; 6 | -------------------------------------------------------------------------------- /static_src/components/notification.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import keymirror from "keymirror"; 4 | import Action from "./action.jsx"; 5 | import { entityHealth } from "../constants.js"; 6 | 7 | const STATUSES = Object.assign( 8 | {}, 9 | entityHealth, 10 | keymirror({ 11 | info: null 12 | }) 13 | ); 14 | 15 | const propTypes = { 16 | message: PropTypes.node, 17 | status: PropTypes.oneOf(Object.keys(STATUSES)), 18 | actions: PropTypes.arrayOf(PropTypes.object), 19 | onDismiss: PropTypes.func 20 | }; 21 | 22 | const defaultProps = { 23 | message: "There was a problem", 24 | status: entityHealth.warning, 25 | actions: [], 26 | onDismiss: () => {} 27 | }; 28 | 29 | export default class Notification extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | 33 | this.onCloseClick = this.onCloseClick.bind(this); 34 | } 35 | 36 | onCloseClick(ev) { 37 | ev.preventDefault(); 38 | this.props.onDismiss(ev); 39 | } 40 | 41 | render() { 42 | let actionElements; 43 | 44 | if (this.props.actions.length) { 45 | actionElements = this.props.actions.map((action, i) => ( 46 | 53 | {action.text} 54 | 55 | )); 56 | } 57 | 58 | return ( 59 |
    64 |
    65 |

    66 | {this.props.message} 67 |

    68 | {actionElements} 69 | 75 | Close 76 | 77 |
    78 |
    79 | ); 80 | } 81 | } 82 | 83 | Notification.propTypes = propTypes; 84 | Notification.defaultProps = defaultProps; 85 | -------------------------------------------------------------------------------- /static_src/components/page_header.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | children: PropTypes.node, 6 | title: PropTypes.node.isRequired 7 | }; 8 | 9 | export default class PageHeader extends React.Component { 10 | get actions() { 11 | if (!this.props.children) { 12 | return null; 13 | } 14 | 15 | return
    {this.props.children}
    ; 16 | } 17 | 18 | render() { 19 | return ( 20 |
    21 |

    22 | {this.props.title} 23 |

    24 | {this.actions} 25 |
    26 | ); 27 | } 28 | } 29 | 30 | PageHeader.propTypes = propTypes; 31 | -------------------------------------------------------------------------------- /static_src/components/panel.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | title: PropTypes.string 6 | }; 7 | 8 | const defaultProps = { 9 | title: "Default title" 10 | }; 11 | 12 | export default class Panel extends React.Component { 13 | render() { 14 | let panelHed = null; 15 | if (this.props.title != "") { 16 | panelHed =

    {this.props.title}

    ; 17 | } 18 | 19 | return ( 20 |
    21 | {panelHed} 22 |
    {this.props.children}
    23 |
    24 | ); 25 | } 26 | } 27 | 28 | Panel.propTypes = propTypes; 29 | Panel.defaultProps = defaultProps; 30 | -------------------------------------------------------------------------------- /static_src/components/panel_actions.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const ALIGN_TYPES = ["left", "right", "both"]; 5 | 6 | const propTypes = { 7 | children: PropTypes.any, 8 | align: PropTypes.oneOf(ALIGN_TYPES) 9 | }; 10 | const defaultProps = { 11 | children: [], 12 | align: ALIGN_TYPES[0] 13 | }; 14 | 15 | export default class PanelActions extends React.Component { 16 | render() { 17 | return ( 18 | 19 | {this.props.children} 20 | 21 | ); 22 | } 23 | } 24 | 25 | PanelActions.propTypes = propTypes; 26 | PanelActions.defaultProps = defaultProps; 27 | -------------------------------------------------------------------------------- /static_src/components/panel_block.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | children: PropTypes.any 6 | }; 7 | const defaultProps = { 8 | children: null 9 | }; 10 | 11 | export default class PanelBlock extends React.Component { 12 | render() { 13 | return
    {this.props.children}
    ; 14 | } 15 | } 16 | 17 | PanelBlock.propTypes = propTypes; 18 | PanelBlock.defaultProps = defaultProps; 19 | -------------------------------------------------------------------------------- /static_src/components/panel_documentation.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | 5 | const propTypes = { 6 | children: PropTypes.any, 7 | description: PropTypes.bool 8 | }; 9 | const defaultProps = { 10 | children: null, 11 | description: false 12 | }; 13 | 14 | export default class PanelDocumentation extends React.Component { 15 | render() { 16 | const classes = classNames("panel-documentation", { 17 | "panel-documentation-desc": this.props.description 18 | }); 19 | 20 | return
    {this.props.children}
    ; 21 | } 22 | } 23 | 24 | PanelDocumentation.propTypes = propTypes; 25 | PanelDocumentation.defaultProps = defaultProps; 26 | -------------------------------------------------------------------------------- /static_src/components/panel_group.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | columns: PropTypes.number, 6 | children: PropTypes.any 7 | }; 8 | const defaultProps = { 9 | columns: 0, 10 | children: null 11 | }; 12 | 13 | export default class PanelGroup extends React.Component { 14 | render() { 15 | let gridClass = ""; 16 | if (this.props.columns !== 0) { 17 | gridClass = `grid-width-${this.props.columns}`; 18 | } 19 | return ( 20 |
    {this.props.children}
    21 | ); 22 | } 23 | } 24 | 25 | PanelGroup.propTypes = propTypes; 26 | PanelGroup.defaultProps = defaultProps; 27 | -------------------------------------------------------------------------------- /static_src/components/panel_header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class PanelHeader extends React.Component { 4 | render() { 5 | return
    {this.props.children}
    ; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static_src/components/panel_row.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | 5 | const STYLES = [ 6 | "bordered", 7 | "boxed", 8 | "clean", // TODO this should be reconciled with panel-row and panel-row-bordered 9 | "none" 10 | ]; 11 | 12 | const propTypes = { 13 | id: PropTypes.string, 14 | className: PropTypes.string, 15 | children: PropTypes.any, 16 | styleClass: PropTypes.oneOf(STYLES) 17 | }; 18 | 19 | const defaultProps = {}; 20 | 21 | export default class PanelRow extends React.Component { 22 | render() { 23 | const { styleClass, id, children, className } = this.props; 24 | const classes = classNames(className, { 25 | [`panel-row-${styleClass}`]: styleClass, 26 | "panel-row": styleClass !== "boxed" 27 | }); 28 | 29 | return ( 30 |
    31 | {children} 32 |
    33 | ); 34 | } 35 | } 36 | 37 | PanelRow.propTypes = propTypes; 38 | PanelRow.defaultProps = defaultProps; 39 | -------------------------------------------------------------------------------- /static_src/components/resource_usage.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import formatBytes from "../util/format_bytes"; 4 | import Stat from "./stat.jsx"; 5 | 6 | const propTypes = { 7 | formGuid: PropTypes.string, 8 | max: PropTypes.number, 9 | min: PropTypes.number, 10 | onChange: PropTypes.func, 11 | title: PropTypes.string.isRequired, 12 | amountUsed: PropTypes.number, 13 | amountTotal: PropTypes.number, 14 | byteWarningThreshold: PropTypes.number, 15 | secondaryInfo: PropTypes.node 16 | }; 17 | 18 | const defaultProps = { 19 | amountUsed: 0, 20 | amountTotal: 0, 21 | byteWarningThreshold: 500000, 22 | onChange: () => {} 23 | }; 24 | 25 | export default class ResourceUsage extends React.Component { 26 | available() { 27 | return formatBytes(this.props.amountTotal - this.props.amountUsed); 28 | } 29 | 30 | statState(used, total) { 31 | if (total - used < this.props.byteWarningThreshold) return "warning"; 32 | return "success"; 33 | } 34 | 35 | render() { 36 | const props = this.props; 37 | 38 | let properties = { 39 | title: props.title, 40 | primaryStat: props.amountTotal, 41 | secondaryInfo: props.secondaryInfo, 42 | editable: props.editable, 43 | max: props.max, 44 | min: props.min, 45 | onChange: props.onChange, 46 | name: props.name, 47 | formGuid: props.formGuid 48 | }; 49 | 50 | if (props.amountUsed && props.amountTotal) { 51 | properties = { 52 | ...properties, 53 | primaryStat: props.amountUsed, 54 | secondaryInfo: {this.available()} available, 55 | statState: this.statState(props.amountUsed, props.amountTotal) 56 | }; 57 | } 58 | 59 | return ; 60 | } 61 | } 62 | 63 | ResourceUsage.propTypes = propTypes; 64 | ResourceUsage.defaultProps = defaultProps; 65 | -------------------------------------------------------------------------------- /static_src/components/router/route_provider.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RouterStore from "../../stores/router_store.js"; 3 | import Loading from "../loading.jsx"; 4 | 5 | class RouteProvider extends React.Component { 6 | constructor() { 7 | super(); 8 | 9 | this.state = RouterStore.component; 10 | 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | RouterStore.addChangeListener(this.onChange); 16 | } 17 | 18 | componentWillUnmount() { 19 | RouterStore.removeChangeListener(this.onChange); 20 | } 21 | 22 | onChange() { 23 | this.setState({ ...RouterStore.component }); 24 | } 25 | 26 | render() { 27 | const { component: Component, props } = this.state; 28 | return Component ? : ; 29 | } 30 | } 31 | 32 | export default RouteProvider; 33 | -------------------------------------------------------------------------------- /static_src/components/service_count_status.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import CountStatus from "./count_status.jsx"; 4 | import ServiceInstanceStore from "../stores/service_instance_store.js"; 5 | import { entityHealth } from "../constants.js"; 6 | import { appInstanceHealth, worstAppInstanceState } from "../util/health"; 7 | 8 | const propTypes = { 9 | serviceCount: PropTypes.number, 10 | services: PropTypes.array 11 | }; 12 | 13 | const defaultProps = { 14 | serviceCount: 0, 15 | services: [] 16 | }; 17 | 18 | export default class ServiceCountStatus extends React.Component { 19 | render() { 20 | const props = this.props; 21 | let health = entityHealth.inactive; 22 | 23 | if (props.services.length) { 24 | health = appInstanceHealth( 25 | worstAppInstanceState( 26 | props.services.map(si => ServiceInstanceStore.getMappedAppState(si)) 27 | ) 28 | ); 29 | } 30 | 31 | return ( 32 | 38 | ); 39 | } 40 | } 41 | 42 | ServiceCountStatus.propTypes = propTypes; 43 | ServiceCountStatus.defaultProps = defaultProps; 44 | -------------------------------------------------------------------------------- /static_src/components/service_instance_list.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import ComplexList from "./complex_list.jsx"; 4 | import ServiceInstance from "./service_instance.jsx"; 5 | 6 | const propTypes = { 7 | currentAppGuid: PropTypes.string.isRequired, 8 | serviceInstances: PropTypes.array, 9 | bound: PropTypes.bool, 10 | empty: PropTypes.bool, 11 | titleElement: PropTypes.element 12 | }; 13 | 14 | const defaultProps = { 15 | serviceInstances: [], 16 | bound: false, 17 | empty: false 18 | }; 19 | 20 | export default class ServiceInstanceList extends React.Component { 21 | render() { 22 | // TODO, when react implements returning unwraped arrays, move ComplexList 23 | // to container of this. 24 | let content =
    ; 25 | 26 | if (this.props.empty) { 27 | content = ( 28 | No services} 31 | /> 32 | ); 33 | } else { 34 | content = ( 35 | 36 | {this.props.serviceInstances.map(serviceInstance => ( 37 | 43 | ))} 44 | 45 | ); 46 | } 47 | 48 | return content; 49 | } 50 | } 51 | 52 | ServiceInstanceList.propTypes = propTypes; 53 | ServiceInstanceList.defaultProps = defaultProps; 54 | -------------------------------------------------------------------------------- /static_src/components/service_list.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Renders a list of services 3 | */ 4 | import PropTypes from "prop-types"; 5 | import React from "react"; 6 | import ServiceListItem from "./service_list_item.jsx"; 7 | import ServiceStore from "../stores/service_store.js"; 8 | 9 | const propTypes = { 10 | services: PropTypes.array 11 | }; 12 | 13 | const empty = services => !ServiceStore.loading && !services.length; 14 | 15 | let content = null; 16 | 17 | const ServiceList = ({ services }) => { 18 | if (empty(services)) { 19 | content =

    No services

    ; 20 | } else if (services.length) { 21 | content = ( 22 |
    23 | {services.map(service => { 24 | const { 25 | servicePlans, 26 | guid, 27 | label, 28 | description, 29 | updated_at 30 | } = service; 31 | 32 | return ( 33 | 41 | ); 42 | })} 43 |
    44 | ); 45 | } 46 | 47 | return
    {content}
    ; 48 | }; 49 | 50 | ServiceList.propTypes = propTypes; 51 | ServiceList.defaultProps = {}; 52 | 53 | export default ServiceList; 54 | -------------------------------------------------------------------------------- /static_src/components/service_list_item.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import ElasticLine from "./elastic_line.jsx"; 4 | import ElasticLineItem from "./elastic_line_item.jsx"; 5 | import ServicePlanList from "./service_plan_list.jsx"; 6 | import formatDateTime from "../util/format_date.js"; 7 | 8 | const propTypes = { 9 | guid: PropTypes.string, 10 | label: PropTypes.string, 11 | description: PropTypes.string, 12 | updatedAt: PropTypes.string, 13 | servicePlans: PropTypes.array 14 | }; 15 | 16 | const ServiceListItem = ({ 17 | guid, 18 | label, 19 | description, 20 | updatedAt, 21 | servicePlans 22 | }) => ( 23 |
    24 | 25 | 26 |

    27 | {label} 28 |

    29 | {description} 30 |
    31 | 32 | Updated {formatDateTime(updatedAt)} 33 | 34 |
    35 | 36 |
    37 | ); 38 | 39 | ServiceListItem.propTypes = propTypes; 40 | 41 | export default ServiceListItem; 42 | -------------------------------------------------------------------------------- /static_src/components/service_plan.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Action from "./action.jsx"; 4 | 5 | const propTypes = { 6 | cost: PropTypes.string, 7 | onAddInstance: PropTypes.func, 8 | plan: PropTypes.shape({ 9 | guid: PropTypes.string, 10 | name: PropTypes.string, 11 | description: PropTypes.string 12 | }) 13 | }; 14 | 15 | // This should be removed when solution is setup 16 | // for service instance multiple params (e.g. cdn-route) or services 17 | // that only return information via the cf cli (e.g. space-deployer). 18 | const CF_CLI_SERVICE_PLAN_LIST = [ 19 | "cdn-route", 20 | "space-auditor", 21 | "space-deployer", 22 | "oauth-client" 23 | ]; 24 | 25 | class ServicePlan extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.handleClick = this.handleClick.bind(this); 30 | } 31 | 32 | handleClick() { 33 | const { plan, onAddInstance } = this.props; 34 | 35 | onAddInstance(plan.guid); 36 | } 37 | 38 | get buttonText() { 39 | let text; 40 | const { plan } = this.props; 41 | 42 | if (CF_CLI_SERVICE_PLAN_LIST.indexOf(plan.name) === -1) { 43 | text = "Create service instance"; 44 | } else { 45 | text = "Display documentation link"; 46 | } 47 | return text; 48 | } 49 | 50 | render() { 51 | const { plan } = this.props; 52 | 53 | return ( 54 | 55 | {plan.name} 56 | {plan.description} 57 | 58 | 63 | {this.buttonText} 64 | 65 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | ServicePlan.propTypes = propTypes; 72 | 73 | export default ServicePlan; 74 | -------------------------------------------------------------------------------- /static_src/components/space_count_status.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import CountStatus from "./count_status.jsx"; 4 | 5 | const propTypes = { 6 | spaces: PropTypes.array 7 | }; 8 | 9 | const defaultProps = { 10 | spaces: [] 11 | }; 12 | 13 | export default class SpaceCountStatus extends React.Component { 14 | render() { 15 | return ; 16 | } 17 | } 18 | 19 | SpaceCountStatus.propTypes = propTypes; 20 | SpaceCountStatus.defaultProps = defaultProps; 21 | -------------------------------------------------------------------------------- /static_src/components/system_error_message.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | const propTypes = { 6 | error: PropTypes.object, 7 | inline: PropTypes.bool.isRequired 8 | }; 9 | 10 | const defaultProps = { 11 | error: null, 12 | inline: true 13 | }; 14 | 15 | export default class SystemErrorMessage extends Component { 16 | get errorMessage() { 17 | const { error } = this.props; 18 | const message = (error && error.message) || error.description; 19 | 20 | return message ? this.knownMessage(message) : this.shortDefaultMessage; 21 | } 22 | 23 | get statusCode() { 24 | return (this.props.error && this.props.error.status_code) || 500; 25 | } 26 | 27 | get shortDefaultMessage() { 28 | return ( 29 | 30 | The system returned an error ("status {this.statusCode}"). If you didn't 31 | expect this, contact support. 32 | You can also try again. 33 | 34 | ); 35 | } 36 | 37 | knownMessage(message) { 38 | return `The system returned an error, ${message}. Please try again`; 39 | } 40 | 41 | render() { 42 | const { error, inline } = this.props; 43 | 44 | if (!error) { 45 | return null; 46 | } 47 | 48 | return ( 49 |
    55 | {this.errorMessage} 56 |
    57 | ); 58 | } 59 | } 60 | 61 | SystemErrorMessage.propTypes = propTypes; 62 | 63 | SystemErrorMessage.defaultProps = defaultProps; 64 | -------------------------------------------------------------------------------- /static_src/components/user_provider.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import UserStore from "../stores/user_store.js"; 4 | 5 | const userProvider = Component => { 6 | class UserProvider extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { currentUser: null }; 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | 14 | getChildContext() { 15 | return { currentUser: this.state.currentUser }; 16 | } 17 | 18 | componentDidMount() { 19 | UserStore.addChangeListener(this.onChange); 20 | } 21 | 22 | componentWillUnmount() { 23 | UserStore.removeChangeListener(this.onChange); 24 | } 25 | 26 | onChange() { 27 | this.setState({ 28 | currentUser: UserStore.currentUser 29 | }); 30 | } 31 | 32 | render() { 33 | return ; 34 | } 35 | } 36 | 37 | UserProvider.childContextTypes = { 38 | currentUser: PropTypes.object 39 | }; 40 | 41 | return UserProvider; 42 | }; 43 | 44 | export default userProvider; 45 | -------------------------------------------------------------------------------- /static_src/components/user_role_control.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const propTypes = { 5 | userId: PropTypes.string.isRequired, 6 | roleName: PropTypes.string.isRequired, 7 | roleKey: PropTypes.string.isRequired, 8 | value: PropTypes.bool, 9 | enableControl: PropTypes.bool, 10 | onChange: PropTypes.func 11 | }; 12 | 13 | const dangerousRole = "org_manager"; 14 | 15 | const warningMessage = 16 | "Performing this action will remove your ability to adjust user roles! Are you sure you want to continue?"; 17 | 18 | export default class UserRoleControl extends React.Component { 19 | constructor(props, context) { 20 | super(props, context); 21 | 22 | this._handleChange = this._handleChange.bind(this); 23 | } 24 | 25 | userSelfRemovingOrgManager(role, removing) { 26 | const { userId } = this.props; 27 | const currentUserId = this.context.currentUser.user_id; 28 | 29 | return userId === currentUserId && role === dangerousRole && removing; 30 | } 31 | 32 | _handleChange(ev) { 33 | const { roleKey, onChange } = this.props; 34 | const { checked, name } = ev.target; 35 | let shouldContinue = true; 36 | 37 | if (this.userSelfRemovingOrgManager(name, !checked)) { 38 | shouldContinue = window.confirm(warningMessage); 39 | } 40 | 41 | if (shouldContinue) { 42 | onChange(roleKey, checked); 43 | } 44 | } 45 | 46 | render() { 47 | const { roleKey, roleName, userId, value, enableControl } = this.props; 48 | const inputId = roleKey + userId; 49 | 50 | return ( 51 | 52 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | UserRoleControl.contextTypes = { 69 | currentUser: PropTypes.object 70 | }; 71 | 72 | UserRoleControl.propTypes = propTypes; 73 | UserRoleControl.defaultProps = { 74 | value: false, 75 | enableControl: false, 76 | onChange: function() {} 77 | }; 78 | -------------------------------------------------------------------------------- /static_src/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Main content 3 | */ 4 | 5 | .main { 6 | padding: 20px; 7 | } 8 | @media (min-width: 768px) { 9 | .main { 10 | padding-right: 40px; 11 | padding-left: 40px; 12 | } 13 | } 14 | .main .page-header { 15 | margin-top: 0; 16 | } 17 | 18 | /* 19 | * Override WDS fancy checkboxes/radios 20 | **/ 21 | input[type="checkbox"]:not(.fancy), 22 | input[type="radio"]:not(.fancy) { 23 | -webkit-appearance: checkbox; 24 | display: inline; 25 | left: 0; 26 | opacity: 1; 27 | position: static; 28 | margin-left: 0.25rem; 29 | margin-right: 0.5rem; 30 | width: auto; 31 | } 32 | 33 | input[type="checkbox"]:checked + label::before { 34 | display: none; 35 | } 36 | 37 | input[type="checkbox"] + label::before, 38 | input[type="radio"] + label::before { 39 | display: none; 40 | } 41 | -------------------------------------------------------------------------------- /static_src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from "flux"; 2 | 3 | import { trackAction } from "./util/analytics"; 4 | 5 | /* eslint-disable no-alert, no-console */ 6 | function logAction(action) { 7 | console.log("::action::", action); 8 | } 9 | /* eslint-enable no-alert, no-console */ 10 | 11 | function addSourceType(srcObj, srcType) { 12 | return Object.assign({}, srcObj, { 13 | source: srcType 14 | }); 15 | } 16 | 17 | class AppDispatcher extends Dispatcher { 18 | // User agent initiated actions that generally require data fetching 19 | // State mutations are related to core data domains like orgs, spaces, etc 20 | handleViewAction(srcAction) { 21 | const action = addSourceType(srcAction, "VIEW_ACTION"); 22 | this.dispatch(action); 23 | logAction(action); 24 | trackAction(action); 25 | } 26 | 27 | // UI actions are things like clicking to expand a menu 28 | // State side affects should just be UI related state 29 | handleUIAction(srcAction) { 30 | const action = addSourceType(srcAction, "UI_ACTION"); 31 | this.dispatch(action); 32 | logAction(action); 33 | trackAction(action); 34 | } 35 | 36 | // Server actions come from the network/API 37 | handleServerAction(srcAction) { 38 | const action = addSourceType(srcAction, "SERVER_ACTION"); 39 | this.dispatch(action); 40 | logAction(action); 41 | trackAction(action); 42 | } 43 | } 44 | 45 | export default new AppDispatcher(); 46 | -------------------------------------------------------------------------------- /static_src/img/dashboard-uaa-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloud-gov/cg-dashboard/6675bf8e832eb99e79cce73ed8a62dd940ea83aa/static_src/img/dashboard-uaa-icon.jpg -------------------------------------------------------------------------------- /static_src/main.js: -------------------------------------------------------------------------------- 1 | import "cloudgov-style/css/base.css"; 2 | import "cloudgov-style/css/cloudgov-style.css"; 3 | import "./css/main.css"; 4 | import "./css/overrides.css"; 5 | // Icon used in cg-uaa. 6 | import "./img/dashboard-uaa-icon.jpg"; 7 | 8 | import React from "react"; 9 | import ReactDOM from "react-dom"; 10 | 11 | import axios from "axios"; 12 | import { Router } from "director"; 13 | 14 | import { trackPageView } from "./util/analytics.js"; 15 | import routes, { checkAuth, clearErrors, notFound } from "./routes"; 16 | 17 | import MainContainer from "./components/main_container.jsx"; 18 | import RouteProvider from "./components/router/route_provider.jsx"; 19 | 20 | const initCSRFHeader = metaTag => { 21 | if (metaTag) { 22 | axios.defaults.headers.common["X-CSRF-Token"] = metaTag.content; 23 | } 24 | }; 25 | 26 | const cRouter = { 27 | run(routeConfig, renderEl) { 28 | const router = new Router(routeConfig); 29 | router.configure({ 30 | async: true, 31 | before: [clearErrors, checkAuth], 32 | notfound: notFound, 33 | on: () => { 34 | trackPageView(window.location.hash); 35 | } 36 | }); 37 | 38 | ReactDOM.render( 39 | 40 | 41 | , 42 | renderEl 43 | ); 44 | 45 | router.init("/"); 46 | } 47 | }; 48 | 49 | initCSRFHeader(document.querySelector('meta[name="gorilla.csrf.Token"]')); 50 | 51 | cRouter.run(routes, document.getElementById("root")); 52 | -------------------------------------------------------------------------------- /static_src/models/quicklook.js: -------------------------------------------------------------------------------- 1 | import Immutable from "immutable"; 2 | 3 | const defaults = { 4 | error: null, 5 | isLoaded: false, 6 | open: false 7 | }; 8 | 9 | /* eslint-disable new-cap */ 10 | export default class Quicklook extends Immutable.Record(defaults, "Quicklook") { 11 | /* eslint-enable new-cap */ 12 | } 13 | -------------------------------------------------------------------------------- /static_src/skins/cg/angle-arrow-down-primary.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static_src/skins/cg/angle-arrow-up-primary-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static_src/skins/cg/home_page/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Panel from "dashboard/components/panel.jsx"; 4 | import PanelGroup from "dashboard/components/panel_group.jsx"; 5 | import InfoActivities from "./info_activities.jsx"; 6 | import InfoEnvironments from "./info_environments.jsx"; 7 | import InfoSandbox from "./info_sandbox.jsx"; 8 | import InfoStructure from "./info_structure.jsx"; 9 | 10 | export const panels = [ 11 | () => ( 12 | 13 | {[InfoActivities, InfoStructure, InfoSandbox, InfoEnvironments].map( 14 | (Tile, i) => { 15 | if (i % 2 === 0) { 16 | return ( 17 |
    18 | 19 | 20 | 21 |
    22 | ); 23 | } 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | )} 31 |
    32 | ) 33 | ]; 34 | 35 | export default {}; 36 | -------------------------------------------------------------------------------- /static_src/skins/cg/home_page/info_activities.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | className: PropTypes.string 6 | }; 7 | 8 | const defaultProps = { 9 | className: "" 10 | }; 11 | 12 | const InfoActivities = ({ className }) => ( 13 |
    14 |

    A few things you can do here

    15 |
      16 |
    • See recent log events for your apps.
    • 17 |
    • Create and bind service instances for your apps.
    • 18 |
    • Create and bind routes for your apps.
    • 19 |
    • Change memory allocation and number of instances for your apps.
    • 20 |
    • Restart your apps.
    • 21 |
    • Manage permissions for users of your orgs and spaces.
    • 22 |
    23 |
    24 | ); 25 | 26 | InfoActivities.propTypes = propTypes; 27 | 28 | InfoActivities.defaultProps = defaultProps; 29 | 30 | export default InfoActivities; 31 | -------------------------------------------------------------------------------- /static_src/skins/cg/home_page/info_environments.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | className: PropTypes.string 6 | }; 7 | 8 | const defaultProps = { 9 | className: "" 10 | }; 11 | 12 | const InfoEnvironment = ({ className }) =>
    ; 13 | 14 | InfoEnvironment.propTypes = propTypes; 15 | 16 | InfoEnvironment.defaultProps = defaultProps; 17 | 18 | export default InfoEnvironment; 19 | -------------------------------------------------------------------------------- /static_src/skins/cg/home_page/info_sandbox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | className: PropTypes.string 6 | }; 7 | 8 | const defaultProps = { 9 | className: "" 10 | }; 11 | 12 | const InfoSandbox = ({ className }) => ( 13 |
    14 |

    Looking at an empty sandbox?

    15 |

    16 | 17 | Try making a “hello world” app 18 | . 19 |

    20 |

    21 | Then see{" "} 22 | 23 | what else you can do 24 | {" "} 25 | and the{" "} 26 | 27 | sandbox policies 28 | . 29 |

    30 |
    31 | ); 32 | 33 | InfoSandbox.propTypes = propTypes; 34 | 35 | InfoSandbox.defaultProps = defaultProps; 36 | 37 | export default InfoSandbox; 38 | -------------------------------------------------------------------------------- /static_src/skins/cg/home_page/info_structure.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const propTypes = { 5 | className: PropTypes.string 6 | }; 7 | 8 | const defaultProps = { 9 | className: "" 10 | }; 11 | 12 | const InfoStructure = ({ className }) => ( 13 |
    14 |

    Basic cloud.gov structure

    15 | 44 |
    45 | ); 46 | 47 | InfoStructure.propTypes = propTypes; 48 | 49 | InfoStructure.defaultProps = defaultProps; 50 | 51 | export default InfoStructure; 52 | -------------------------------------------------------------------------------- /static_src/skins/cg/icon-dot-gov.svg: -------------------------------------------------------------------------------- 1 | dot gov icon -------------------------------------------------------------------------------- /static_src/skins/cg/icon-https.svg: -------------------------------------------------------------------------------- 1 | https icon -------------------------------------------------------------------------------- /static_src/skins/cg/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export { default as header } from "./header"; 4 | 5 | import * as homePage from "./home_page"; 6 | 7 | export { homePage }; 8 | 9 | import InfoLogs from "dashboard/components/info_logs.jsx"; 10 | 11 | export const config = { 12 | footer: { 13 | author_note: A United States government platform, 14 | code_note: ( 15 | 16 | Open source and in the public domain 17 | 18 | ), 19 | disclaimer_note: ( 20 | 21 | Vulnerability disclosure policy 22 | 23 | ), 24 | links: [ 25 | { 26 | text: "cloud.gov home", 27 | url: "https://cloud.gov" 28 | }, 29 | { 30 | text: "Get help for customer issues", 31 | url: "https://cloud.gov/docs/help/#support-for-people-who-use-cloud-gov" 32 | }, 33 | { 34 | text: "Built and maintained by 18F", 35 | url: "https://18f.gsa.gov/" 36 | } 37 | ] 38 | }, 39 | docs: { 40 | cli: "https://cloud.gov/docs/getting-started/setup/", 41 | concepts_roles: "https://docs.cloudfoundry.org/concepts/roles.html", 42 | concepts_spaces: "https://cloud.gov/docs/getting-started/concepts/", 43 | deploying_apps: "https://cloud.gov/docs/getting-started/your-first-deploy/", 44 | use: "https://cloud.gov/overview/overview/using-cloudgov-paas/", 45 | invite_user: "https://cloud.gov/docs/apps/managing-teammates/", 46 | roles: 47 | "https://cloud.gov/docs/apps/managing-teammates/#give-roles-to-a-teammate", 48 | managed_services: "https://cloud.gov/docs/apps/managed-services/", 49 | status: "https://cloudgov.statuspage.io/", 50 | contact: "https://cloud.gov/docs/help/" 51 | }, 52 | snippets: { 53 | logs: InfoLogs 54 | }, 55 | github: { 56 | url: "https://github.com/18F/cg-dashboard" 57 | }, 58 | platform: { 59 | name: "cloud.gov", 60 | api_host: "api.fr.cloud.gov", 61 | logs: { 62 | name: "logs.fr.cloud.gov", 63 | url: "https://logs.fr.cloud.gov" 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /static_src/stores/domain_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store for domain data. Will store and update domain data on changes from UI and 3 | * server. 4 | */ 5 | 6 | import Immutable from "immutable"; 7 | 8 | import BaseStore from "./base_store.js"; 9 | import cfApi from "../util/cf_api.js"; 10 | import { domainActionTypes } from "../constants.js"; 11 | 12 | class DomainStore extends BaseStore { 13 | constructor() { 14 | super(); 15 | this.storeData = new Immutable.List(); 16 | this.subscribe(() => this.handleAction.bind(this)); 17 | } 18 | 19 | handleAction(action) { 20 | switch (action.type) { 21 | case domainActionTypes.DOMAIN_FETCH: { 22 | cfApi.fetchPrivateDomain(action.domainGuid); 23 | break; 24 | } 25 | 26 | case domainActionTypes.DOMAIN_RECEIVED: { 27 | this.merge("guid", action.domain, changed => { 28 | if (changed) this.emitChange(); 29 | }); 30 | break; 31 | } 32 | 33 | default: 34 | break; 35 | } 36 | } 37 | } 38 | 39 | export default new DomainStore(); 40 | -------------------------------------------------------------------------------- /static_src/stores/error_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store for generic error data. 3 | */ 4 | 5 | import Immutable from "immutable"; 6 | 7 | import BaseStore from "./base_store.js"; 8 | import { errorActionTypes } from "../constants.js"; 9 | 10 | export class ErrorStore extends BaseStore { 11 | constructor() { 12 | super(); 13 | this.maxErrors = 3; 14 | this.subscribe(() => this.handleAction.bind(this)); 15 | } 16 | 17 | checkForMaxFetchErrors() { 18 | const errs = this.getAll(); 19 | if (errs.length >= this.maxErrors) { 20 | // If too many errors, clear them and provide a generic fetch one. 21 | this.storeData = new Immutable.List(); 22 | const genericFetchError = { 23 | description: "Connection issue, please try again" 24 | }; 25 | this.push(genericFetchError); 26 | } 27 | } 28 | 29 | handleAction(action) { 30 | switch (action.type) { 31 | case errorActionTypes.NOTIFY: { 32 | const err = Object.assign({}, action.err); 33 | // Put this error at the top, since it is considered higher priority 34 | this.storeData = this.storeData.unshift(err); 35 | break; 36 | } 37 | 38 | case errorActionTypes.IMPORTANT_FETCH: { 39 | const err = Object.assign({}, { description: action.msg }, action.err); 40 | this.push(err); 41 | this.checkForMaxFetchErrors(); 42 | break; 43 | } 44 | 45 | case errorActionTypes.DISMISS: { 46 | const errIdx = this.getAll().findIndex(err => err === action.err); 47 | if (errIdx) { 48 | // TODO little unsafe to access data here? 49 | this.storeData = this.storeData.delete(errIdx); 50 | this.emitChange(); 51 | } 52 | break; 53 | } 54 | 55 | case errorActionTypes.CLEAR: { 56 | this.storeData = new Immutable.List(); 57 | this.emitChange(); 58 | break; 59 | } 60 | default: 61 | break; 62 | } 63 | } 64 | } 65 | 66 | export default new ErrorStore(); 67 | -------------------------------------------------------------------------------- /static_src/stores/login_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store to hold and update login status. 3 | */ 4 | 5 | import BaseStore from "./base_store.js"; 6 | import { loginActionTypes } from "../constants.js"; 7 | 8 | // Babel doesn't like extending native types with `class`, so use prototype 9 | // inheritence. 10 | export function LoginError(err) { 11 | this.err = err; 12 | 13 | // Figure out description 14 | if (err.description) { 15 | this.description = err.description; 16 | } else if (err.message) { 17 | this.description = `An error occurred while trying to check your authorization. You may need to 18 | login again. Error: ${err.message}`; 19 | } else { 20 | this.description = 21 | "An error occurred while trying to check your authorization. You may need " + 22 | "to login again."; 23 | } 24 | } 25 | 26 | LoginError.prototype = Object.create(Error.prototype); 27 | LoginError.prototype.constructor = Error; 28 | 29 | export class LoginStore extends BaseStore { 30 | constructor() { 31 | super(); 32 | this.subscribe(() => this.handleAction.bind(this)); 33 | // TODO this should probably be false, but we need to account for the 34 | // initial state (not loaded/unknown). 35 | this.isAuthenticated = true; 36 | this.error = null; 37 | } 38 | 39 | handleAction(action) { 40 | switch (action.type) { 41 | case loginActionTypes.FETCH_STATUS: 42 | // Reset any error 43 | this.error = null; 44 | this.emitChange(); 45 | break; 46 | 47 | case loginActionTypes.RECEIVED_STATUS: 48 | this.isAuthenticated = action.authStatus.status === "authorized"; 49 | this.emitChange(); 50 | break; 51 | 52 | case loginActionTypes.ERROR_STATUS: { 53 | // Login status is unknown. If we have a login status, leave it as is 54 | // and hope things go smooth. If necessary, the action caller is 55 | // responsible for notifying the user of an error. 56 | let error = action.err; 57 | if (!(action.err instanceof LoginError)) { 58 | error = new LoginError(action.err); 59 | } 60 | 61 | this.error = error; 62 | break; 63 | } 64 | 65 | default: 66 | break; 67 | } 68 | } 69 | 70 | isLoggedIn() { 71 | return !!this.isAuthenticated; 72 | } 73 | } 74 | 75 | export default new LoginStore(); 76 | -------------------------------------------------------------------------------- /static_src/stores/page_store.js: -------------------------------------------------------------------------------- 1 | import BaseStore from "./base_store.js"; 2 | import { pageActionTypes } from "../constants.js"; 3 | 4 | class PageStore extends BaseStore { 5 | constructor() { 6 | super(); 7 | this.isLoading = false; 8 | this.subscribe(() => this.handleAction.bind(this)); 9 | } 10 | 11 | handleAction(action) { 12 | switch (action.type) { 13 | case pageActionTypes.PAGE_LOAD_STARTED: { 14 | this.isLoading = true; 15 | this.emitChange(); 16 | break; 17 | } 18 | 19 | case pageActionTypes.PAGE_LOAD_ERROR: 20 | case pageActionTypes.PAGE_LOAD_SUCCESS: { 21 | this.isLoading = false; 22 | this.emitChange(); 23 | break; 24 | } 25 | 26 | default: 27 | break; 28 | } 29 | } 30 | } 31 | 32 | export default new PageStore(); 33 | -------------------------------------------------------------------------------- /static_src/stores/quota_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store for quota data. 3 | */ 4 | 5 | import Immutable from "immutable"; 6 | 7 | import BaseStore from "./base_store.js"; 8 | import cfApi from "../util/cf_api.js"; 9 | import { quotaActionTypes } from "../constants.js"; 10 | 11 | class QuotaStore extends BaseStore { 12 | constructor() { 13 | super(); 14 | this.storeData = new Immutable.List(); 15 | this.subscribe(() => this.handleAction.bind(this)); 16 | } 17 | 18 | handleAction(action) { 19 | switch (action.type) { 20 | case quotaActionTypes.ORGS_QUOTAS_FETCH: { 21 | this.load([cfApi.fetchOrgsQuotas()]); 22 | this.emitChange(); 23 | break; 24 | } 25 | 26 | case quotaActionTypes.ORGS_QUOTAS_RECEIVED: { 27 | const quotas = action.quotas; 28 | this.mergeMany("guid", quotas, changed => { 29 | if (changed) this.emitChange(); 30 | }); 31 | break; 32 | } 33 | 34 | case quotaActionTypes.SPACES_QUOTAS_FETCH: { 35 | this.load([cfApi.fetchSpacesQuotas()]); 36 | this.emitChange(); 37 | break; 38 | } 39 | 40 | case quotaActionTypes.SPACES_QUOTAS_RECEIVED: { 41 | const quotas = action.quotas; 42 | this.mergeMany("guid", quotas, changed => { 43 | if (changed) this.emitChange(); 44 | }); 45 | break; 46 | } 47 | 48 | default: 49 | break; 50 | } 51 | } 52 | } 53 | 54 | export default new QuotaStore(); 55 | -------------------------------------------------------------------------------- /static_src/stores/router_store.js: -------------------------------------------------------------------------------- 1 | import BaseStore from "./base_store.js"; 2 | import { routerActionTypes } from "../constants.js"; 3 | 4 | class RouterStore extends BaseStore { 5 | constructor() { 6 | super(); 7 | 8 | this.routeComponent = {}; 9 | this.subscribe(() => this.registerToActions.bind(this)); 10 | } 11 | 12 | registerToActions(action) { 13 | const { type, data } = action; 14 | 15 | switch (type) { 16 | case routerActionTypes.NAVIGATE: 17 | this.routeComponent = Object.assign({}, { ...data }); 18 | this.emitChange(); 19 | break; 20 | default: 21 | break; 22 | } 23 | } 24 | 25 | get component() { 26 | return this.routeComponent; 27 | } 28 | } 29 | 30 | export default new RouterStore(); 31 | -------------------------------------------------------------------------------- /static_src/stores/service_binding_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store for service bindings data. Bindings are what link apps to service 3 | * instances. 4 | */ 5 | 6 | import BaseStore from "./base_store.js"; 7 | import { serviceActionTypes } from "../constants.js"; 8 | 9 | export class ServiceBindingStore extends BaseStore { 10 | constructor() { 11 | super(); 12 | this.subscribe(() => this.handleAction.bind(this)); 13 | this.isFetching = false; 14 | } 15 | 16 | get loading() { 17 | return this.isFetching; 18 | } 19 | 20 | getAllByApp(appGuid) { 21 | return this.getAll().filter(binding => binding.app_guid === appGuid); 22 | } 23 | 24 | handleAction(action) { 25 | switch (action.type) { 26 | case serviceActionTypes.SERVICE_BINDINGS_FETCH: { 27 | this.isFetching = true; 28 | this.emitChange(); 29 | break; 30 | } 31 | 32 | case serviceActionTypes.SERVICE_BINDINGS_RECEIVED: { 33 | this.isFetching = false; 34 | const bindings = action.serviceBindings; 35 | this.mergeMany("guid", bindings, () => {}); 36 | this.emitChange(); 37 | break; 38 | } 39 | 40 | case serviceActionTypes.SERVICE_BIND: { 41 | // TODO store the biding-in-progress state within a new serviceBinding 42 | break; 43 | } 44 | 45 | case serviceActionTypes.SERVICE_UNBIND: { 46 | const binding = this.get(action.serviceBinding.guid); 47 | const unbindingService = Object.assign({}, binding, { 48 | unbinding: true 49 | }); 50 | this.merge("guid", unbindingService); 51 | break; 52 | } 53 | 54 | case serviceActionTypes.SERVICE_BOUND: { 55 | const binding = action.serviceBinding; 56 | this.merge("guid", binding); 57 | break; 58 | } 59 | 60 | case serviceActionTypes.SERVICE_UNBOUND: { 61 | const binding = this.get(action.serviceBinding.guid); 62 | if (binding) { 63 | this.delete(binding.guid, () => this.emitChange()); 64 | } 65 | break; 66 | } 67 | 68 | default: 69 | break; 70 | } 71 | } 72 | } 73 | 74 | export default new ServiceBindingStore(); 75 | -------------------------------------------------------------------------------- /static_src/stores/service_store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Store for services data. Will store and update services data on changes from 3 | * UI and server. 4 | */ 5 | 6 | import AppDispatcher from "../dispatcher"; 7 | import BaseStore from "./base_store.js"; 8 | import { serviceActionTypes } from "../constants.js"; 9 | import ServicePlanStore from "./service_plan_store.js"; 10 | 11 | export class ServiceStore extends BaseStore { 12 | constructor() { 13 | super(); 14 | this.subscribe(() => this.handleAction.bind(this)); 15 | this.isFetchingAll = false; 16 | } 17 | 18 | get loading() { 19 | return this.isFetchingAll; 20 | } 21 | 22 | handleAction(action) { 23 | switch (action.type) { 24 | case serviceActionTypes.SERVICES_FETCH: { 25 | this.isFetchingAll = true; 26 | break; 27 | } 28 | 29 | case serviceActionTypes.SERVICES_RECEIVED: { 30 | this.isFetchingAll = false; 31 | AppDispatcher.waitFor([ServicePlanStore.dispatchToken]); 32 | const services = action.services; 33 | this.mergeMany("guid", services, () => { 34 | // Always emitchange as fetch state was changed. 35 | this.emitChange(); 36 | }); 37 | break; 38 | } 39 | 40 | default: 41 | break; 42 | } 43 | } 44 | } 45 | 46 | export default new ServiceStore(); 47 | -------------------------------------------------------------------------------- /static_src/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:jasmine/recommended"], 3 | "env": { 4 | "jasmine": true 5 | }, 6 | "globals": { 7 | "sinon": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["jasmine"], 17 | "rules": { 18 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 19 | "func-names": [0, "always"], 20 | "jasmine/no-spec-dupes": [2, "branch"], 21 | "jasmine/no-suite-dupes": [2, "branch"], 22 | "one-var": 0, 23 | "one-var-declaration-per-line": 0, 24 | "prefer-arrow-callback": 0, 25 | // TODO(jonathaningram): the following rules are turned off to quiet the 26 | // linter during a transition between an old version of airbnb/javascript, 27 | // introducing prettier and upgrading to the latest version of 28 | // airbnb/javascript. 29 | // The existence of these rules does not indicate that they should be 30 | // ignored. It simply means that they should be slowly re-enabled as the 31 | // codebase is updated. 32 | "jasmine/new-line-before-expect": 0, 33 | "jasmine/new-line-between-declarations": 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /static_src/test/functional/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "globals": { 4 | "browser": true, 5 | "sinon": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static_src/test/functional/overview_quicklook.spec.js: -------------------------------------------------------------------------------- 1 | import OrgQuicklookElement from "./pageobjects/org_quicklook.element"; 2 | 3 | describe("Overview page", function() { 4 | it("navigates to page", function() { 5 | browser.url("/"); 6 | }); 7 | 8 | it("has a title", function() { 9 | expect(browser.getTitle()).toBe("cloud.gov dashboard"); 10 | }); 11 | 12 | it("has a page header", function() { 13 | const pageHeader = browser.element(".test-page-header-title"); 14 | expect(pageHeader.getText()).toBe("Overview"); 15 | }); 16 | 17 | describe("quicklook", function() { 18 | let quicklookElement; 19 | 20 | it("exists", function() { 21 | quicklookElement = new OrgQuicklookElement( 22 | browser, 23 | browser.element(".test-panel-row-organizations") 24 | ); 25 | expect(quicklookElement.isVisible()).toBe(true); 26 | }); 27 | 28 | it("has org name", function() { 29 | const orgName = quicklookElement.title(); 30 | expect(orgName).toBe("fake-cf"); 31 | }); 32 | 33 | it("is collapsed", function() { 34 | expect(quicklookElement.isExpanded()).toBe(false); 35 | }); 36 | 37 | it("is clicked", function() { 38 | quicklookElement.expand(); 39 | }); 40 | 41 | it("is expanded", function() { 42 | expect(quicklookElement.isExpanded()).toBe(true); 43 | }); 44 | 45 | it("has 2 rows", function() { 46 | expect(quicklookElement.rows().length).toBe(2); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /static_src/test/functional/pageobjects/base.element.js: -------------------------------------------------------------------------------- 1 | // https://www.martinfowler.com/bliki/PageObject.html 2 | 3 | /** 4 | * BaseFederalistElement 5 | * 6 | * An API to handle WebElement JSON objects by leveraging the [webdriverio 7 | * protocol methods](http://webdriver.io/api.html). This implements 8 | * (incomplete) some of the helper methods from webdriverio to make it easier 9 | * to deal with a single component. 10 | **/ 11 | 12 | import assert from "assert"; 13 | 14 | export default class BaseElement { 15 | constructor(browser, webElementOrSelector) { 16 | this.browser = browser; 17 | 18 | let webElement = webElementOrSelector; 19 | if (typeof webElementOrSelector === "string") { 20 | browser.waitForExist(webElementOrSelector); 21 | webElement = browser.element(webElementOrSelector); 22 | } 23 | 24 | assert( 25 | webElement.value, 26 | `Element '${webElement.selector}' does not exist in the DOM.` 27 | ); 28 | this.webElementId = webElement.value.ELEMENT; 29 | } 30 | 31 | webElement() { 32 | return this.browser.elementIdElement(this.webElementId); 33 | } 34 | 35 | element(selector) { 36 | return this.browser.elementIdElement(this.webElementId, selector); 37 | } 38 | 39 | elements(selector) { 40 | return this.browser.elementIdElements(this.webElementId, selector).value; 41 | } 42 | 43 | isVisible() { 44 | return this.browser.elementIdDisplayed(this.webElementId).value; 45 | } 46 | 47 | click() { 48 | return this.browser.elementIdClick(this.webElementId); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /static_src/test/functional/pageobjects/breadcrumbs.element.js: -------------------------------------------------------------------------------- 1 | import BaseElement from "./base.element"; 2 | 3 | const breadcrumbs = '[data-test="breadcrumbs"]'; 4 | 5 | const selectors = { 6 | primary: breadcrumbs, 7 | overview: `${breadcrumbs} [data-test="overview"]`, 8 | org: `${breadcrumbs} [data-test="org"]`, 9 | space: `${breadcrumbs} [data-test="space"]` 10 | }; 11 | 12 | export default class Breadcrumbs extends BaseElement { 13 | overviewLink() { 14 | return this.element(selectors.overview); 15 | } 16 | 17 | orgLink() { 18 | return this.element(selectors.org); 19 | } 20 | 21 | spaceLink() { 22 | return this.element(selectors.space); 23 | } 24 | 25 | goToSpace() { 26 | const spaceLink = this.spaceLink(); 27 | spaceLink.click(); 28 | } 29 | 30 | exists() { 31 | return !!this.isVisible(); 32 | } 33 | } 34 | 35 | Breadcrumbs.primarySelector = selectors.primary; 36 | -------------------------------------------------------------------------------- /static_src/test/functional/pageobjects/global_errors.element.js: -------------------------------------------------------------------------------- 1 | import BaseElement from "./base.element"; 2 | import NotificationElement from "./notification.element"; 3 | 4 | // https://www.martinfowler.com/bliki/PageObject.html 5 | // 6 | // Represents a DOM element for making assertions against. This makes it 7 | // easier to abstract some of the webdriver details from the UI component. 8 | 9 | // TODO attach to class as static property 10 | const selectors = { 11 | notifications: ".test-notification", 12 | firstNotification: ".test-notification:first-child" 13 | }; 14 | 15 | export default class GlobalErrorsElement extends BaseElement { 16 | notifications() { 17 | return this.elements(selectors.notifications); 18 | } 19 | 20 | firstNotification() { 21 | return new NotificationElement( 22 | this.browser, 23 | this.element(selectors.firstNotification) 24 | ); 25 | } 26 | 27 | exists() { 28 | return !!this.isVisible(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /static_src/test/functional/pageobjects/notification.element.js: -------------------------------------------------------------------------------- 1 | import BaseElement from "./base.element"; 2 | 3 | // https://www.martinfowler.com/bliki/PageObject.html 4 | // 5 | // Represents a DOM element for making assertions against. This makes it 6 | // easier to abstract some of the webdriver details from the UI component. 7 | 8 | // TODO attach to class as static property 9 | const selectors = { 10 | notificationMessage: ".test-notification-message", 11 | notificationAction: ".test-notification-action", 12 | notificationDismiss: ".test-notification-dismiss" 13 | }; 14 | 15 | export default class NotificationElement extends BaseElement { 16 | message() { 17 | return this.element(selectors.notificationMessage).getText(); 18 | } 19 | 20 | dismissAction() { 21 | return this.element(selectors.notificationDismiss); 22 | } 23 | 24 | refreshAction() { 25 | return this.element(selectors.notificationAction); 26 | } 27 | 28 | notificationStatus() {} 29 | 30 | exists() { 31 | return !!this.isVisible(); 32 | } 33 | 34 | dismiss() { 35 | this.dismissAction().click(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /static_src/test/functional/pageobjects/org_quicklook.element.js: -------------------------------------------------------------------------------- 1 | import BaseElement from "./base.element"; 2 | 3 | // https://www.martinfowler.com/bliki/PageObject.html 4 | // 5 | // Represents a OrgQuicklookElement for making assertions against. This makes it 6 | // easier to abstract some of the webdriver details from the UI component. 7 | 8 | // TODO attach to class as static property 9 | const selectors = { 10 | title: ".test-org-quicklook-title", 11 | spaceQuicklookRow: ".test-space-quicklook" 12 | }; 13 | 14 | export default class OrgQuicklookElement extends BaseElement { 15 | title() { 16 | return this.element(selectors.title).getText(); 17 | } 18 | 19 | isExpanded() { 20 | return !!this.rows().length; 21 | } 22 | 23 | rows() { 24 | // TODO this should return new SpaceQuicklookRowElements 25 | return this.elements(selectors.spaceQuicklookRow); 26 | } 27 | 28 | expand() { 29 | this.click(); 30 | browser.waitUntil(() => this.isExpanded(), 2000); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /static_src/test/global_setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jasmine/no-global-setup,no-console */ 2 | require("babel-polyfill"); 3 | 4 | import jasmineEnzyme from "jasmine-enzyme"; 5 | import LoginStore from "../stores/login_store"; 6 | import UserStore from "../stores/user_store"; 7 | 8 | beforeEach(function() { 9 | jasmineEnzyme(); 10 | }); 11 | 12 | beforeEach(() => { 13 | // Any call to console.warn or console.error should fail the test. If 14 | // console.warn or console.error is expected, they should be stubbed 15 | // appropriately. 16 | sinon 17 | .stub(console, "warn") 18 | .throws( 19 | new Error( 20 | "Unexpected call to console.warn during a test. Please add an expectation or fix the test." 21 | ) 22 | ); 23 | // TODO enable the same for console.error 24 | }); 25 | 26 | afterEach(function() { 27 | console.warn.restore(); 28 | }); 29 | 30 | // TODO Stub out axios.{get,delete,patch,post,put}, all async calls should be 31 | // stubbed or mocked, otherwise it's an error. 32 | 33 | // TODO Stores should have a different singleton strategy so that state can 34 | // be cleared and managed consistently in tests. Currently, all the singleton 35 | // stores are listening to dispatch events, which often cause events to be 36 | // processed twice. 37 | // UserStore is only an issue because the store calls cfApi. cfApi calls should 38 | // be moved to the actions. 39 | LoginStore.unsubscribe(); 40 | UserStore.unsubscribe(); 41 | -------------------------------------------------------------------------------- /static_src/test/perf/budgets.js: -------------------------------------------------------------------------------- 1 | // TODO pull these values from ./config.json to remove duplication. 2 | module.exports = { 3 | "speed-index-metric": { 4 | expectedValue: 12000, 5 | goal: 3000 6 | }, 7 | "estimated-input-latency": { 8 | expectedValue: 30, 9 | goal: 16 10 | }, 11 | "time-to-interactive": { 12 | expectedValue: 10000, 13 | goal: 4000 14 | }, 15 | "total-byte-weight": { 16 | expectedValue: 2000000, 17 | goal: 1600000 18 | }, 19 | "dom-size": { 20 | expectedValue: 1500, 21 | goal: 1000 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /static_src/test/perf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "passes": [ 3 | { 4 | "recordNetwork": true, 5 | "recordTrace": true, 6 | "pauseBeforeTraceEndMs": 500, 7 | "useThrottling": true, 8 | "gatherers": [ 9 | "url", 10 | "image-usage", 11 | "content-width", 12 | "dobetterweb/domstats" 13 | ] 14 | } 15 | ], 16 | 17 | "audits": [ 18 | "speed-index-metric", 19 | "estimated-input-latency", 20 | "time-to-interactive", 21 | "user-timings", 22 | "screenshots", 23 | "critical-request-chains", 24 | "byte-efficiency/total-byte-weight", 25 | "dobetterweb/dom-size" 26 | ], 27 | 28 | "aggregations": [ 29 | { 30 | "name": "Speed", 31 | "description": "", 32 | "scored": false, 33 | "categorizable": false, 34 | "items": [ 35 | { 36 | "audits": { 37 | "speed-index-metric": { 38 | "expectedValue": 12000, 39 | "weight": 1 40 | }, 41 | "time-to-interactive": { 42 | "expectedValue": 10000, 43 | "weight": 1 44 | }, 45 | "estimated-input-latency": { 46 | "expectedValue": 30, 47 | "weight": 1 48 | }, 49 | "total-byte-weight": { 50 | "expectedValue": 2000000, 51 | "weight": 2 52 | }, 53 | "dom-size": { 54 | "expectedValue": 1500, 55 | "weight": 2 56 | } 57 | } 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /static_src/test/perf/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "static_src/test/perf", 3 | "spec_files": ["**/*.spec.js"] 4 | } 5 | -------------------------------------------------------------------------------- /static_src/test/server/authstatus.js: -------------------------------------------------------------------------------- 1 | module.exports = function authstatus(smocks) { 2 | smocks.route({ 3 | id: "authstatus", 4 | label: "Auth status", // label is optional 5 | path: "/v2/authstatus", 6 | 7 | handler: function(req, reply) { 8 | reply({ 9 | status: "authorized" 10 | }); 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/app_routes.js: -------------------------------------------------------------------------------- 1 | const appRoutes = [ 2 | { 3 | metadata: { 4 | guid: "228342e9-2c9d-4874-95fe-0a06ac5ceb43", 5 | url: "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43", 6 | created_at: "2016-12-05T21:11:03Z", 7 | updated_at: "2016-12-05T21:11:03Z" 8 | }, 9 | entity: { 10 | host: "fake-adfake-node", 11 | path: 0, 12 | domain_guid: "97435c2f-d5bb-4c10-8393-55d7d7169932", 13 | space_guid: "82af0edb-8540-4064-82f2-d74df612b794", 14 | service_instance_guid: null, 15 | port: null, 16 | domain_url: "/v2/shared_domains/97435c2f-d5bb-4c10-8393-55d7d7169932", 17 | space_url: "/v2/spaces/82af0edb-8540-4064-82f2-d74df612b794", 18 | apps_url: "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43/apps", 19 | route_mappings_url: 20 | "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43/route_mappings" 21 | } 22 | } 23 | ]; 24 | 25 | module.exports = appRoutes; 26 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/app_stats.js: -------------------------------------------------------------------------------- 1 | const appStats = [ 2 | { 3 | guid: "2f684200-b9da-4ea6-a3c8-01a1df5ef2d3", // Fake 4 | 5 | "0": { 6 | state: "RUNNING", 7 | stats: { 8 | name: "fake-adfake-node", 9 | uris: ["fake-adfake-node.apps.cloud.gov"], 10 | host: "10.10.2.101", 11 | port: 62853, 12 | uptime: 673981, 13 | mem_quota: 85983232, 14 | disk_quota: 2147483648, 15 | fds_quota: 16384, 16 | usage: { 17 | time: "2017-01-12 23:56:54 +0000", 18 | cpu: 0, 19 | mem: 19812352, 20 | disk: 54493184 21 | } 22 | } 23 | }, 24 | "1": { 25 | state: "RUNNING", 26 | stats: { 27 | name: "fake-adfake-node", 28 | uris: ["fake-adfake-node.apps.cloud.gov"], 29 | host: "10.10.1.116", 30 | port: 62946, 31 | uptime: 541975, 32 | mem_quota: 85983232, 33 | disk_quota: 2147483648, 34 | fds_quota: 16384, 35 | usage: { 36 | time: "2017-01-12 23:56:54 +0000", 37 | cpu: 0, 38 | mem: 9162752, 39 | disk: 54489088 40 | } 41 | } 42 | } 43 | } 44 | ]; 45 | 46 | module.exports = appStats; 47 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/organization_memory_usage.js: -------------------------------------------------------------------------------- 1 | const memoryUsage = { memory_usage_in_mb: 292 }; 2 | 3 | module.exports = memoryUsage; 4 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/organization_quota_definitions.js: -------------------------------------------------------------------------------- 1 | const organizationQuotaDefinitions = [ 2 | { 3 | metadata: { 4 | guid: "f7963421-c06e-4847-9913-bcd0e6048fa2", 5 | url: "/v2/quota_definitions/f7963421-c06e-4847-9913-bcd0e6048fa2", 6 | created_at: "2015-04-08T18:55:21Z", 7 | updated_at: "2015-12-08T21:08:53Z" 8 | }, 9 | entity: { 10 | name: "fakeDevOps", 11 | non_basic_services_allowed: true, 12 | total_services: 10000, 13 | total_routes: 1000, 14 | total_private_domains: -1, 15 | memory_limit: 40960, 16 | trial_db_allowed: false, 17 | instance_memory_limit: -1, 18 | app_instance_limit: -1, 19 | app_task_limit: -1, 20 | total_service_keys: -1, 21 | total_reserved_route_ports: 0 22 | } 23 | } 24 | ]; 25 | 26 | module.exports = organizationQuotaDefinitions; 27 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/shared_domains.js: -------------------------------------------------------------------------------- 1 | const sharedDomains = [ 2 | { 3 | metadata: { 4 | guid: "97435c2f-d5bb-4c10-8393-55d7d7169932", 5 | url: "/v2/shared_domains/97435c2f-d5bb-4c10-8393-55d7d7169932", 6 | created_at: "2015-10-12T16:33:19Z", 7 | updated_at: "2016-12-30T20:28:29Z" 8 | }, 9 | entity: { 10 | name: "fake-apps.cloud.gov", 11 | router_group_guid: null, 12 | router_group_type: null 13 | } 14 | } 15 | ]; 16 | 17 | module.exports = sharedDomains; 18 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/space_quota_definitions.js: -------------------------------------------------------------------------------- 1 | const spaceQuotaDefinitions = [ 2 | { 3 | total_results: 1, 4 | total_pages: 1, 5 | prev_url: null, 6 | next_url: null, 7 | resources: [ 8 | { 9 | metadata: { 10 | guid: "66404ac5-9979-4e80-9457-85afa3af929a", 11 | url: 12 | "/v2/space_quota_definitions/66404ac5-9979-4e80-9457-85afa3af929a", 13 | created_at: "2016-02-11T17:44:57Z", 14 | updated_at: "2016-02-27T01:28:06Z" 15 | }, 16 | entity: { 17 | name: "sandbox_quota", 18 | organization_guid: "4a962676-e687-46c4-95f4-7a83712065c6", 19 | non_basic_services_allowed: true, 20 | total_services: 10, 21 | total_routes: 10, 22 | memory_limit: 1024, 23 | instance_memory_limit: -1, 24 | app_instance_limit: -1, 25 | app_task_limit: 5, 26 | total_service_keys: 1000, 27 | total_reserved_route_ports: -1, 28 | organization_url: 29 | "/v2/organizations/4a962676-e687-46c4-95f4-7a83712065c6", 30 | spaces_url: 31 | "/v2/space_quota_definitions/66404ac5-9979-4e80-9457-85afa3af929a/spaces" 32 | } 33 | } 34 | ] 35 | } 36 | ]; 37 | 38 | module.exports = spaceQuotaDefinitions; 39 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/space_routes.js: -------------------------------------------------------------------------------- 1 | const spaceRoutes = [ 2 | { 3 | metadata: { 4 | guid: "228342e9-2c9d-4874-95fe-0a06ac5ceb43", 5 | url: "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43", 6 | created_at: "2016-12-05T21:11:03Z", 7 | updated_at: "2016-12-05T21:11:03Z" 8 | }, 9 | entity: { 10 | host: "fake-adfake-node", 11 | path: 0, 12 | domain_guid: "97435c2f-d5bb-4c10-8393-55d7d7169932", 13 | space_guid: "82af0edb-8540-4064-82f2-d74df612b794", 14 | service_instance_guid: null, 15 | port: null, 16 | domain_url: "/v2/shared_domains/97435c2f-d5bb-4c10-8393-55d7d7169932", 17 | space_url: "/v2/spaces/82af0edb-8540-4064-82f2-d74df612b794", 18 | apps_url: "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43/apps", 19 | route_mappings_url: 20 | "/v2/routes/228342e9-2c9d-4874-95fe-0a06ac5ceb43/route_mappings" 21 | } 22 | }, 23 | { 24 | metadata: { 25 | guid: "b0a7fd97-ce44-4aeb-9bd8-204d17cdf9ef", 26 | url: "/v2/routes/b0a7fd97-ce44-4aeb-9bd8-204d17cdf9ef", 27 | created_at: "2016-12-22T22:06:17Z", 28 | updated_at: "2016-12-22T22:06:17Z" 29 | }, 30 | entity: { 31 | host: "fake-adfake-node-crashed", 32 | path: 0, 33 | domain_guid: "97435c2f-d5bb-4c10-8393-55d7d7169932", 34 | space_guid: "82af0edb-8540-4064-82f2-d74df612b794", 35 | service_instance_guid: null, 36 | port: null, 37 | domain_url: "/v2/shared_domains/97435c2f-d5bb-4c10-8393-55d7d7169932", 38 | space_url: "/v2/spaces/82af0edb-8540-4064-82f2-d74df612b794", 39 | apps_url: "/v2/routes/b0a7fd97-ce44-4aeb-9bd8-204d17cdf9ef/apps", 40 | route_mappings_url: 41 | "/v2/routes/b0a7fd97-ce44-4aeb-9bd8-204d17cdf9ef/route_mappings" 42 | } 43 | } 44 | ]; 45 | 46 | module.exports = spaceRoutes; 47 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/uaa_roles.js: -------------------------------------------------------------------------------- 1 | const uaaRoles = { 2 | uaa_admin: { 3 | id: "cca7537f-601d-48c4-9705-4583ba54ea4c", 4 | externalId: "fake-person-uaa-admin@gsa.gov", 5 | meta: { 6 | version: 0, 7 | created: "2016-09-16T13:24:31.423Z", 8 | lastModified: "2016-09-16T13:24:31.423Z" 9 | }, 10 | userName: "fake-person-uaa-admin@gsa.gov", 11 | name: { 12 | familyName: "gsa.gov", 13 | givenName: "fake-person-uaa-admin" 14 | }, 15 | emails: [ 16 | { 17 | value: "fake-person-uaa-admin@gsa.gov", 18 | primary: false 19 | } 20 | ], 21 | groups: [ 22 | { 23 | value: "88e68451-dc2e-413d-963b-848740512e01c1a23019", 24 | display: "cloud_controller.admin", 25 | type: "DIRECT" 26 | } 27 | ], 28 | approvals: [], 29 | active: true, 30 | verified: true, 31 | origin: "gsa.gov", 32 | zoneId: "uaa", 33 | passwordLastModified: "2016-09-16T13:24:31.000Z", 34 | previousLogonTime: 1489612053883, 35 | lastLogonTime: 1489612053883, 36 | schemas: ["urn:scim:schemas:core:1.0"] 37 | }, 38 | default: { 39 | id: "bba7537f-601d-48c4-9705-4583ba54ea4b", 40 | externalId: "fake-personb@gsa.gov", 41 | meta: { 42 | version: 0, 43 | created: "2016-09-16T13:24:31.423Z", 44 | lastModified: "2016-09-16T13:24:31.423Z" 45 | }, 46 | userName: "fake-personb@gsa.gov", 47 | name: { 48 | familyName: "gsa.gov", 49 | givenName: "fake-personb" 50 | }, 51 | emails: [ 52 | { 53 | value: "fake-personb@gsa.gov", 54 | primary: false 55 | } 56 | ], 57 | groups: [], 58 | approvals: [], 59 | active: true, 60 | verified: true, 61 | origin: "gsa.gov", 62 | zoneId: "uaa", 63 | passwordLastModified: "2016-09-16T13:24:31.000Z", 64 | previousLogonTime: 1489612053883, 65 | lastLogonTime: 1489612053883, 66 | schemas: ["urn:scim:schemas:core:1.0"] 67 | } 68 | }; 69 | 70 | module.exports = uaaRoles; 71 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/user_invite_responses.js: -------------------------------------------------------------------------------- 1 | const userInviteResponses = { 2 | default: { 3 | status: "success", 4 | userGuid: "4541c882-fake-invited-new-user" 5 | }, 6 | "fake-new-user@domain.com": { 7 | status: "success", 8 | userGuid: "4541c882-fake-invited-fake-new-user" 9 | } 10 | }; 11 | 12 | module.exports = userInviteResponses; 13 | -------------------------------------------------------------------------------- /static_src/test/server/fixtures/user_role_org_add_new_role.js: -------------------------------------------------------------------------------- 1 | const userRoleOrgAddNewRole = function(orgGuid) { 2 | return { 3 | metadata: { 4 | guid: "" + orgGuid + "", 5 | url: "/v2/organizations/" + orgGuid + "", 6 | created_at: "2016-06-09T17:52:26Z", 7 | updated_at: "2016-06-09T17:52:26Z" 8 | }, 9 | entity: { 10 | name: "cf", 11 | billing_enabled: false, 12 | quota_definition_guid: "01817050-964e-4632-a9bd-fbcda5f8d14c", 13 | status: "active", 14 | default_isolation_segment_guid: null, 15 | quota_definition_url: 16 | "/v2/quota_definitions/01817050-964e-4632-a9bd-fbcda5f8d14c", 17 | spaces_url: "/v2/organizations/" + orgGuid + "/spaces", 18 | domains_url: "/v2/organizations/" + orgGuid + "/domains", 19 | private_domains_url: "/v2/organizations/" + orgGuid + "/private_domains", 20 | users_url: "/v2/organizations/" + orgGuid + "/users", 21 | managers_url: "/v2/organizations/" + orgGuid + "/managers", 22 | billing_managers_url: 23 | "/v2/organizations/" + orgGuid + "/billing_managers", 24 | auditors_url: "/v2/organizations/" + orgGuid + "/auditors", 25 | app_events_url: "/v2/organizations/" + orgGuid + "/app_events", 26 | space_quota_definitions_url: 27 | "/v2/organizations/" + orgGuid + "/space_quota_definitions" 28 | } 29 | }; 30 | }; 31 | 32 | module.exports = userRoleOrgAddNewRole; 33 | -------------------------------------------------------------------------------- /static_src/test/server/index.js: -------------------------------------------------------------------------------- 1 | import hapi from "hapi"; 2 | import inert from "inert"; 3 | import smocks from "smocks"; 4 | 5 | import authstatus from "./authstatus"; 6 | import api from "./api"; 7 | 8 | // configure smocks as a hapi plugin 9 | const smocksplugin = require("smocks/hapi").toPlugin(); 10 | const pkg = require("../../../package.json"); 11 | 12 | smocks.id("cg-dashboard-testing"); 13 | 14 | // add auth status route 15 | authstatus(smocks); 16 | // add all api routes 17 | api(smocks); 18 | 19 | /* 20 | * Starts the server. 21 | * 22 | * @param port (optional) the system will choose a port automatically 23 | * @param cb (optional) callback to notify when server starts 24 | **/ 25 | export function start(...args) { 26 | const cb = args.pop(); 27 | const port = args[0]; 28 | 29 | const server = new hapi.Server(); 30 | 31 | server.connection({ 32 | port 33 | }); 34 | 35 | smocksplugin.attributes = { 36 | pkg 37 | }; 38 | 39 | server.register([inert, smocksplugin]); 40 | 41 | // serve static assets 42 | server.route({ 43 | method: "get", 44 | path: "/assets/{p*}", 45 | handler: { 46 | directory: { 47 | path: "static/assets" 48 | } 49 | } 50 | }); 51 | 52 | server.route({ 53 | method: "get", 54 | path: "/skins/{p*}", 55 | handler: { 56 | directory: { 57 | path: "static/skins" 58 | } 59 | } 60 | }); 61 | 62 | server.route({ 63 | method: "get", 64 | path: "/{p*}", 65 | handler: { 66 | directory: { 67 | path: "templates/web" 68 | } 69 | } 70 | }); 71 | 72 | // Default callback 73 | function defaultCallback(err) { 74 | if (err) { 75 | throw err; 76 | } 77 | 78 | /* eslint-disable no-console */ 79 | console.log( 80 | `Started smocks server on ${server.info.port}. Visit ${ 81 | server.info.uri 82 | }/_admin to configure.` 83 | ); 84 | /* eslint-enable no-console */ 85 | } 86 | 87 | server.start(cb || defaultCallback); 88 | 89 | return server; 90 | } 91 | 92 | export default {}; 93 | -------------------------------------------------------------------------------- /static_src/test/unit/actions/domain_actions.spec.js: -------------------------------------------------------------------------------- 1 | import "../../global_setup.js"; 2 | 3 | import AppDispatcher from "../../../dispatcher.js"; 4 | import { assertAction, setupViewSpy, setupServerSpy } from "../helpers.js"; 5 | import domainActions from "../../../actions/domain_actions.js"; 6 | import { domainActionTypes } from "../../../constants.js"; 7 | 8 | describe("domainActions", function() { 9 | var sandbox; 10 | 11 | beforeEach(() => { 12 | sandbox = sinon.sandbox.create(); 13 | }); 14 | 15 | afterEach(() => { 16 | sandbox.restore(); 17 | }); 18 | 19 | describe("fetch()", function() { 20 | it("should dispatch a view event of type domain fetch", function() { 21 | var expectedDomainGuid = "xzzzasdflkjz", 22 | expectedParams = { 23 | domainGuid: expectedDomainGuid 24 | }; 25 | 26 | let spy = setupViewSpy(sandbox); 27 | 28 | domainActions.fetch(expectedDomainGuid); 29 | 30 | assertAction(spy, domainActionTypes.DOMAIN_FETCH, expectedParams); 31 | }); 32 | }); 33 | 34 | describe("receivedDomain()", function() { 35 | it("should dispatch a view event of type domain resceived", function() { 36 | var expected = { guid: "asdfavcx1z13c5", name: "al.gov" }, 37 | expectedParams = { 38 | domain: expected 39 | }; 40 | 41 | let spy = setupServerSpy(sandbox); 42 | 43 | domainActions.receivedDomain(expected); 44 | 45 | assertAction(spy, domainActionTypes.DOMAIN_RECEIVED, expectedParams); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /static_src/test/unit/actions/error_actions.spec.js: -------------------------------------------------------------------------------- 1 | import "../../global_setup.js"; 2 | 3 | import { setupUISpy, setupServerSpy } from "../helpers.js"; 4 | import errorActions from "../../../actions/error_actions.js"; 5 | import { errorActionTypes } from "../../../constants.js"; 6 | 7 | describe("errorActions", function() { 8 | let sandbox; 9 | 10 | beforeEach(() => { 11 | sandbox = sinon.sandbox.create(); 12 | }); 13 | 14 | afterEach(() => { 15 | sandbox.restore(); 16 | }); 17 | 18 | describe("dismissError()", function() { 19 | it("should dispatch a server dismiss event with error object", () => { 20 | const err = { description: "error" }; 21 | const dispatchSpy = setupUISpy(sandbox); 22 | 23 | errorActions.dismissError(err); 24 | 25 | expect(dispatchSpy).toHaveBeenCalledOnce(); 26 | const dispatch = dispatchSpy.getCall(0).args[0]; 27 | expect(dispatch.type).toEqual(errorActionTypes.DISMISS); 28 | expect(dispatch.err).toEqual(err); 29 | }); 30 | }); 31 | 32 | describe("importantDataFetchError()", function() { 33 | let dispatchSpy; 34 | let dispatch; 35 | const message = "app broken"; 36 | 37 | beforeEach(() => { 38 | const err = { description: "Server error" }; 39 | dispatchSpy = setupServerSpy(sandbox); 40 | 41 | errorActions.importantDataFetchError(err, message); 42 | 43 | dispatch = dispatchSpy.getCall(0).args[0]; 44 | }); 45 | 46 | it("should dispatch an important fetch error server event", () => { 47 | expect(dispatchSpy).toHaveBeenCalledOnce(); 48 | expect(dispatch.type).toEqual(errorActionTypes.IMPORTANT_FETCH); 49 | }); 50 | 51 | it("should wrap the supplied error message with generic messaging", () => { 52 | expect(dispatch.msg).toEqual( 53 | `There was an issue connecting to the dashboard, ${message}` 54 | ); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /static_src/test/unit/actions/quota_actions.spec.js: -------------------------------------------------------------------------------- 1 | import "../../global_setup.js"; 2 | 3 | import AppDispatcher from "../../../dispatcher.js"; 4 | import { 5 | assertAction, 6 | setupViewSpy, 7 | setupServerSpy, 8 | setupUISpy 9 | } from "../helpers.js"; 10 | import cfApi from "../../../util/cf_api.js"; 11 | import quotaActions from "../../../actions/quota_actions.js"; 12 | import { quotaActionTypes } from "../../../constants.js"; 13 | 14 | describe("quotaActions", function() { 15 | var sandbox; 16 | 17 | beforeEach(() => { 18 | sandbox = sinon.sandbox.create(); 19 | }); 20 | 21 | afterEach(() => { 22 | sandbox.restore(); 23 | }); 24 | 25 | describe("fetchQuotasForAllOrgs()", function() { 26 | it("should dispatch a view event to get all organization quotas", function() { 27 | let spy = setupViewSpy(sandbox); 28 | 29 | quotaActions.fetchQuotasForAllOrgs(); 30 | 31 | assertAction(spy, quotaActionTypes.ORGS_QUOTAS_FETCH); 32 | }); 33 | }); 34 | 35 | describe("receivedQuotasForAllOrgs()", function() { 36 | it("should dispatch a server event to process recieved organizations quotas", function() { 37 | let spy = setupServerSpy(sandbox); 38 | let quotas = [{ guid: "fake-quota-one" }]; 39 | 40 | quotaActions.receivedQuotasForAllOrgs(quotas); 41 | 42 | assertAction(spy, quotaActionTypes.ORGS_QUOTAS_RECEIVED, { quotas }); 43 | }); 44 | }); 45 | 46 | describe("fetchQuotasForAllSpaces()", function() { 47 | it("should dispatch a view event to get all space quotas", function() { 48 | let spy = setupViewSpy(sandbox); 49 | 50 | quotaActions.fetchQuotasForAllSpaces(); 51 | 52 | assertAction(spy, quotaActionTypes.SPACES_QUOTAS_FETCH); 53 | }); 54 | }); 55 | 56 | describe("receivedQuotasForAllSpaces()", function() { 57 | it("should dispatch a server event to process recieved spaces quotas", function() { 58 | let spy = setupServerSpy(sandbox); 59 | let quotas = [{ guid: "fake-quota-one" }]; 60 | 61 | quotaActions.receivedQuotasForAllSpaces(quotas); 62 | 63 | assertAction(spy, quotaActionTypes.SPACES_QUOTAS_RECEIVED, { quotas }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /static_src/test/unit/actions/router_actions.spec.js: -------------------------------------------------------------------------------- 1 | import "../../global_setup"; 2 | import React from "react"; 3 | import { assertAction, setupViewSpy } from "../helpers"; 4 | import routerActions from "../../../actions/router_actions"; 5 | import { routerActionTypes } from "../../../constants"; 6 | 7 | describe("routerActions", () => { 8 | let sandbox; 9 | 10 | beforeEach(() => { 11 | sandbox = sinon.sandbox.create(); 12 | }); 13 | 14 | afterEach(() => { 15 | sandbox.restore(); 16 | }); 17 | 18 | describe("navigate()", () => { 19 | it("dispatches `NAVIGATE` action and passes a component and props", () => { 20 | const props = { some: "data" }; 21 | const component = () =>
    ; 22 | const expected = { component, props }; 23 | const spy = setupViewSpy(sandbox); 24 | 25 | routerActions.navigate(component, props); 26 | 27 | assertAction(spy, routerActionTypes.NAVIGATE, expected); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /static_src/test/unit/components/action/button.spec.jsx: -------------------------------------------------------------------------------- 1 | import "../../../global_setup.js"; 2 | 3 | import React from "react"; 4 | import { shallow } from "enzyme"; 5 | import Button from "../../../../components/action/button.jsx"; 6 | 7 | describe("