├── .editorconfig ├── .gitattributes ├── .gitignore ├── .lgtm ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── components │ ├── BenchmarkConfigsList │ │ └── index.js │ ├── BenchmarkInstancesList │ │ └── index.js │ ├── BenchmarkReportDetails │ │ └── index.js │ ├── BenchmarkReportItem │ │ └── index.js │ ├── BenchmarkReportOverview │ │ └── index.js │ ├── Confirmation │ │ └── index.js │ ├── DropFileZone │ │ └── index.js │ ├── EnvironmentForm │ │ ├── index.js │ │ └── messages.js │ ├── EnvironmentsList │ │ ├── index.js │ │ └── item.js │ ├── ErrorIndicator │ │ └── index.js │ ├── FileExt2EnvForm │ │ └── index.js │ ├── FunctionForm │ │ └── index.js │ ├── FunctionLogForm │ │ └── index.js │ ├── FunctionLogItem │ │ └── index.js │ ├── FunctionTabForm │ │ ├── index.js │ │ └── messages.js │ ├── FunctionTestForm │ │ ├── historyItem.js │ │ ├── index.js │ │ └── requestBodyBuilder.js │ ├── FunctionsList │ │ └── index.js │ ├── KeyValueBuilder │ │ ├── index.js │ │ └── row.js │ ├── KubeWatcherCreateForm │ │ ├── index.js │ │ └── messages.js │ ├── KubeWatcherForm │ │ ├── index.js │ │ ├── item.js │ │ └── messages.js │ ├── LoadingIndicator │ │ ├── Circle.js │ │ ├── Wrapper.js │ │ ├── index.js │ │ └── tests │ │ │ ├── Circle.test.js │ │ │ ├── Wrapper.test.js │ │ │ └── index.test.js │ ├── NavLink │ │ └── index.js │ ├── ProgressBar │ │ ├── Percent.js │ │ ├── ProgressBar.js │ │ ├── Wrapper.js │ │ ├── index.js │ │ └── tests │ │ │ ├── Percent.test.js │ │ │ ├── ProgressBar.test.js │ │ │ ├── Wrapper.test.js │ │ │ └── index.test.js │ ├── Toggle │ │ ├── index.js │ │ ├── option.js │ │ └── tests │ │ │ └── index.test.js │ ├── TriggerHttpCreateForm │ │ ├── index.js │ │ └── messages.js │ ├── TriggerHttpForm │ │ ├── index.js │ │ ├── item.js │ │ └── messages.js │ ├── TriggerMQCreateForm │ │ ├── index.js │ │ └── messages.js │ ├── TriggerMQForm │ │ ├── index.js │ │ ├── item.js │ │ └── messages.js │ ├── TriggerTimerCreateForm │ │ ├── index.js │ │ └── messages.js │ └── TriggerTimerForm │ │ ├── index.js │ │ ├── item.js │ │ └── messages.js ├── containers │ ├── App │ │ ├── constants.js │ │ ├── index.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── index.test.js │ │ │ └── selectors.test.js │ ├── BenchmarkConfigListItem │ │ └── index.js │ ├── BenchmarkConfigPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── BenchmarkConfigsListPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── BenchmarkInstanceListItem │ │ └── index.js │ ├── BenchmarkInstancePage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── BenchmarkInstancesListPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── BenchmarksPage │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── EnvironmentCreatePage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── EnvironmentEditPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── EnvironmentsListPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── EnvironmentsPage │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ ├── FunctionCreatePage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── FunctionEditPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── FunctionListItem │ │ ├── index.js │ │ └── messages.js │ ├── FunctionUploadPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── listItem.js │ │ └── sagas.js │ ├── FunctionsListPage │ │ ├── actions.js │ │ ├── index.js │ │ ├── messages.js │ │ └── sagas.js │ ├── FunctionsPage │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ ├── LanguageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ ├── LocaleToggle │ │ ├── Wrapper.js │ │ ├── index.js │ │ ├── messages.js │ │ └── tests │ │ │ ├── Wrapper.test.js │ │ │ └── index.test.js │ └── NotFoundPage │ │ ├── index.js │ │ ├── messages.js │ │ └── tests │ │ └── index.test.js ├── favicon.ico ├── global-styles.js ├── i18n.js ├── index.html ├── manifest.json ├── messages │ └── index.js ├── reducers.js ├── routes.js ├── store.js ├── tests │ ├── i18n.test.js │ └── store.test.js ├── translations │ ├── en.json │ ├── fr.json │ └── zh.json └── utils │ ├── api.js │ ├── asyncInjectors.js │ ├── confirm.js │ ├── tests │ └── asyncInjectors.test.js │ ├── tprapi.js │ └── util.js ├── docker ├── Dockerfile ├── README.md ├── build.sh ├── fission-ui.conf.template ├── fission-ui.yaml ├── push.sh └── run.sh ├── documentation └── images │ ├── batch-upload.png │ ├── environment-list.png │ ├── function-edit.png │ ├── function-list.png │ └── function-triggers.png ├── internals ├── config.js ├── generators │ ├── component │ │ ├── es6.js.hbs │ │ ├── es6.pure.js.hbs │ │ ├── index.js │ │ ├── messages.js.hbs │ │ ├── stateless.js.hbs │ │ └── test.js.hbs │ ├── container │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── constants.js.hbs │ │ ├── index.js │ │ ├── index.js.hbs │ │ ├── messages.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── sagas.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── selectors.js.hbs │ │ ├── selectors.test.js.hbs │ │ └── test.js.hbs │ ├── index.js │ ├── language │ │ ├── add-locale-data.hbs │ │ ├── app-locale.hbs │ │ ├── format-translation-messages.hbs │ │ ├── index.js │ │ ├── intl-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── translation-messages.hbs │ │ └── translations-json.hbs │ ├── route │ │ ├── index.js │ │ ├── route.hbs │ │ └── routeWithReducer.hbs │ └── utils │ │ └── componentExists.js ├── mocks │ ├── cssModule.js │ └── image.js ├── scripts │ ├── analyze.js │ ├── clean.js │ ├── dependencies.js │ ├── extract-intl.js │ ├── generate-templates-for-linting.js │ ├── helpers │ │ ├── checkmark.js │ │ ├── progress.js │ │ └── xmark.js │ └── npmcheckversion.js ├── testing │ └── test-bundler.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ ├── webpack.dll.babel.js │ └── webpack.prod.babel.js ├── package.json ├── server ├── index.js ├── logger.js └── middlewares │ └── frontendMiddleware.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | .nginx.conf text 53 | 54 | # git config 55 | .gitattributes text 56 | .gitignore text 57 | .gitconfig text 58 | 59 | # code analysis config 60 | .jshintrc text 61 | .jscsrc text 62 | .jshintignore text 63 | .csslintrc text 64 | 65 | # misc config 66 | *.yaml text 67 | *.yml text 68 | .editorconfig text 69 | 70 | # build config 71 | *.npmignore text 72 | *.bowerrc text 73 | 74 | # Heroku 75 | Procfile text 76 | .slugignore text 77 | 78 | # Documentation 79 | *.md text 80 | LICENSE text 81 | AUTHORS text 82 | 83 | 84 | # 85 | ## These files are binary and should be left untouched 86 | # 87 | 88 | # (binary is a macro for -text -diff) 89 | *.png binary 90 | *.jpg binary 91 | *.jpeg binary 92 | *.gif binary 93 | *.ico binary 94 | *.mov binary 95 | *.mp4 binary 96 | *.mp3 binary 97 | *.flv binary 98 | *.fla binary 99 | *.swf binary 100 | *.gz binary 101 | *.zip binary 102 | *.7z binary 103 | *.ttf binary 104 | *.eot binary 105 | *.woff binary 106 | *.pyc binary 107 | *.pdf binary 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /.lgtm: -------------------------------------------------------------------------------- 1 | approvals = 2 2 | pattern = "(?i):shipit:|LGTM" 3 | self_approval_off = true 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 7 5 | - 6 6 | 7 | script: 8 | - npm run test 9 | - npm run build 10 | 11 | notifications: 12 | email: 13 | on_failure: change 14 | slack: 15 | secure: TCSWdu+B2Ir/gedvMUNzghk4Mk761K0orR4FaY+JkyUjYPjCNVw75wp75QEY+LwvBPd6CGzC6QtKcXlC9sdcwn2RDq+jl9MF2OShHdegboA6AVA1w3diDYoODzEvqDjK3jAoAvs9P5IinU3a8fokC2BQw3HmotIpNMvw9Fp1vcsLuLv/FFWexCJkV98F3NIpC2xpK0uUki/H1hun+M23PZkhlKFiBjsp0I1YeCBBDhqLDqkxvVFnKWATp/DI9YMTjg3d9ThqZo3fsg6Rxq/rywZa8vpvDbBfyi6XBTZXaxPcKJHpZwGpxkZQaUGWt22+dsfPFpocULM0g8otuYWCvmfFryyBeLwKT++dVYZWUd5ezB0IeryNBhsz5qC7cLiB56om55IYoylwvG8P8QorE0MaY+siDpZ4gGsvoslFaffohAopQJlP1VzgLW7BiOV3vad3jDXxbAqi7eRo+hD5Q4S2TJ6sczwBtNzQll2Yrr8t6KkdWchGBbDCYtcjH44C2b3tu0aNw/869o5X8OnQIoYJqO5u6UtRx0brcjduYVE9R/EAhXvi5xc/EotEFPqEH16FUjaUAZKDQqTpaUndWn4+rgU/enGwCaq8Omy7kxg+cDbaeM6rncGlGDm8grMGMHJYZHEln2ZDlMg9UlZqlNLq7cQIy/YhFlO+YynSIdk= 16 | 17 | cache: 18 | yarn: true 19 | directories: 20 | - node_modules 21 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | 46 | 47 | # Do not cache sw.js, required for offline-first updates. 48 | 49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform" 50 | Header set Pragma "no-cache" 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/components/BenchmarkConfigsList/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BenchmarkConfigsList 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import LoadingIndicator from 'components/LoadingIndicator'; 9 | import ErrorIndicator from 'components/ErrorIndicator'; 10 | import BenchmarkConfigListItem from 'containers/BenchmarkConfigListItem'; 11 | import { FormattedMessage } from 'react-intl'; 12 | import commonMessages from 'messages'; 13 | 14 | function BenchmarkConfigsList({ loading, error, items, onRemove }) { 15 | if (loading) { 16 | return ; 17 | } 18 | if (error !== false) { 19 | return ; 20 | } 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | { 31 | items.map((item, index) => ( 32 | { onRemove(item); }} /> 33 | )) 34 | } 35 | 36 |
37 | ); 38 | } 39 | 40 | BenchmarkConfigsList.propTypes = { 41 | loading: PropTypes.bool, 42 | error: PropTypes.any, 43 | items: PropTypes.oneOfType([ 44 | PropTypes.object, 45 | PropTypes.array, 46 | ]), 47 | onRemove: PropTypes.func, 48 | }; 49 | 50 | export default BenchmarkConfigsList; 51 | -------------------------------------------------------------------------------- /app/components/BenchmarkInstancesList/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BenchmarkInstancesList 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import LoadingIndicator from 'components/LoadingIndicator'; 9 | import ErrorIndicator from 'components/ErrorIndicator'; 10 | import BenchmarkInstanceListItem from 'containers/BenchmarkInstanceListItem'; 11 | import { FormattedMessage } from 'react-intl'; 12 | import commonMessages from 'messages'; 13 | 14 | function BenchmarkInstancesList({ loading, error, items, onRemove, onRun, onStop }) { 15 | if (loading) { 16 | return ; 17 | } 18 | if (error !== false) { 19 | return ; 20 | } 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | { 32 | items.map((item, index) => ( 33 | { onRemove(item); }} 37 | onRun={() => { onRun(item); }} 38 | onStop={() => { onStop(item); }} 39 | /> 40 | )) 41 | } 42 | 43 |
44 | ); 45 | } 46 | 47 | BenchmarkInstancesList.propTypes = { 48 | loading: PropTypes.bool, 49 | error: PropTypes.any, 50 | items: PropTypes.oneOfType([ 51 | PropTypes.object, 52 | PropTypes.array, 53 | ]), 54 | onRemove: PropTypes.func, 55 | onRun: PropTypes.func, 56 | onStop: PropTypes.func, 57 | }; 58 | 59 | export default BenchmarkInstancesList; 60 | -------------------------------------------------------------------------------- /app/components/BenchmarkReportOverview/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BenchmarkReportOverview 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import { ProgressBar } from 'react-bootstrap'; 10 | import commonMessages from 'messages'; 11 | 12 | function BenchmarkReportOverview({ instance }) { 13 | const { spec } = instance; 14 | const progress = parseInt(spec.progress * 100, 10); 15 | return ( 16 |
17 |
18 |
19 | 20 |
21 | {spec.status} 22 |
23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 36 |
37 | {(new Date(spec.starttimestamp * 1000)).toString()} 38 |
39 |
40 |
41 | 46 |
47 | {(new Date(spec.endtimestamp * 1000)).toString()} 48 |
49 |
50 |
51 | 54 |
55 |
    56 | { 57 | spec.errors.map((item, index) => ( 58 |
  • item
  • 59 | )) 60 | } 61 |
62 |
63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | BenchmarkReportOverview.propTypes = { 70 | instance: PropTypes.object, 71 | }; 72 | 73 | export default BenchmarkReportOverview; 74 | -------------------------------------------------------------------------------- /app/components/Confirmation/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import Modal from 'react-bootstrap/lib/Modal'; 4 | import Button from 'react-bootstrap/lib/Button'; 5 | import { confirmable } from 'react-confirm'; 6 | 7 | class Confirmation extends React.Component { // eslint-disable-line react/prefer-stateless-function 8 | render() { 9 | const { 10 | okLabbel = 'OK', 11 | cancelLabel = 'Cancel', 12 | title, 13 | confirmation, 14 | show, 15 | proceed, 16 | dismiss, 17 | cancel, 18 | enableEscape = true, 19 | } = this.props; 20 | return ( 21 |
22 | 23 | 24 | {title} 25 | 26 | 27 | {confirmation} 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | Confirmation.propTypes = { 40 | okLabbel: PropTypes.string, 41 | cancelLabel: PropTypes.string, 42 | title: PropTypes.string, 43 | confirmation: PropTypes.string, 44 | show: PropTypes.bool, 45 | proceed: PropTypes.func, // called when ok button is clicked. 46 | cancel: PropTypes.func, // called when cancel button is clicked. 47 | dismiss: PropTypes.func, // called when backdrop is clicked or escaped. 48 | enableEscape: PropTypes.bool, 49 | }; 50 | 51 | export default confirmable(Confirmation); 52 | -------------------------------------------------------------------------------- /app/components/DropFileZone/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import commonMessages from 'messages'; 4 | 5 | class DropFileZone extends React.Component { // eslint-disable-line react/prefer-stateless-function 6 | 7 | constructor(props) { 8 | super(props); 9 | this.onDrop = this.onDrop.bind(this); 10 | } 11 | 12 | onDragOver(e) { 13 | const evt = e; 14 | evt.stopPropagation(); 15 | evt.preventDefault(); 16 | evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. 17 | } 18 | 19 | onDrop(evt) { 20 | evt.stopPropagation(); 21 | evt.preventDefault(); 22 | 23 | const files = evt.dataTransfer.files; // FileList object. 24 | // files is a FileList of File objects. List some properties. 25 | this.props.onFilesDropped(Array.from(files)); 26 | } 27 | 28 | style = { 29 | border: '2px dashed #bbb', 30 | borderRadius: '5px', 31 | padding: '25px', 32 | textAlign: 'center', 33 | color: '#bbb', 34 | }; 35 | 36 | render() { 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | DropFileZone.propTypes = { 46 | onFilesDropped: PropTypes.func.isRequired, 47 | }; 48 | 49 | export default DropFileZone; 50 | -------------------------------------------------------------------------------- /app/components/EnvironmentForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * EnvironmentForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Link } from 'react-router'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | // import styled from 'styled-components'; 12 | 13 | class EnvironmentForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 14 | render() { 15 | const { environment, onSave, onChange, nameEditable, onSelectSample, sampleEnabled } = this.props; 16 | 17 | return ( 18 |
19 |
20 | 21 | { nameEditable ? ( 22 | 23 | ) : ( 24 | 25 | ) 26 | } 27 |
28 |
29 | 30 | 31 |
32 | { sampleEnabled && 33 |
34 | 35 | 40 |
41 | } 42 | { ' ' } 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | EnvironmentForm.propTypes = { 51 | environment: React.PropTypes.object, 52 | onSave: React.PropTypes.func, 53 | onChange: React.PropTypes.func.isRequired, 54 | nameEditable: React.PropTypes.bool, 55 | sampleEnabled: React.PropTypes.bool, 56 | onSelectSample: React.PropTypes.func, 57 | }; 58 | 59 | export default EnvironmentForm; 60 | -------------------------------------------------------------------------------- /app/components/EnvironmentForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentForm Messages 3 | * 4 | * This contains all the text for the EnvironmentForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.EnvironmentForm.header', 11 | defaultMessage: 'This is the EnvironmentForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/EnvironmentsList/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * EnvironmentsList 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import LoadingIndicator from 'components/LoadingIndicator'; 9 | import ErrorIndicator from 'components/ErrorIndicator'; 10 | import { FormattedMessage } from 'react-intl'; 11 | import commonMessages from 'messages'; 12 | import Item from './item'; 13 | 14 | // import styled from 'styled-components'; 15 | 16 | function EnvironmentsList({ loading, error, environments, onRemove }) { 17 | if (loading) { 18 | return ; 19 | } 20 | if (error !== false) { 21 | return ; 22 | } 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | { 34 | environments.map((item, index) => ( 35 | { onRemove(item); }} /> 36 | )) 37 | } 38 | 39 |
40 | ); 41 | } 42 | 43 | EnvironmentsList.propTypes = { 44 | loading: PropTypes.bool, 45 | error: PropTypes.any, 46 | environments: PropTypes.oneOfType([ 47 | PropTypes.object, 48 | PropTypes.array, 49 | ]), 50 | onRemove: PropTypes.func, 51 | }; 52 | 53 | export default EnvironmentsList; 54 | -------------------------------------------------------------------------------- /app/components/EnvironmentsList/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * EnvironmentsListItem 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | 10 | import { FormattedMessage } from 'react-intl'; 11 | import { Link } from 'react-router'; 12 | import commonMessages from 'messages'; 13 | 14 | function Item({ item, onRemove }) { 15 | return ( 16 | 17 | { item.name } 18 | { item.image } 19 | 20 | { ' ' } 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | Item.propTypes = { 28 | item: React.PropTypes.object, 29 | onRemove: React.PropTypes.func, 30 | }; 31 | 32 | export default Item; 33 | -------------------------------------------------------------------------------- /app/components/ErrorIndicator/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ErrorIndicator 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | 9 | function ErrorIndicator({ errors }) { 10 | return ( 11 |
12 |
    13 | {errors.map((e, i) =>
  • {e}
  • )} 14 |
15 |
16 | ); 17 | } 18 | 19 | ErrorIndicator.propTypes = { 20 | errors: PropTypes.array, 21 | }; 22 | 23 | export default ErrorIndicator; 24 | -------------------------------------------------------------------------------- /app/components/FileExt2EnvForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import commonMessages from 'messages'; 4 | 5 | class FileExt2EnvForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 6 | render() { 7 | const { environments, fileExt2Env, draftMapping, onDraftMappingChange, onDraftMappingCreate, onRemoveFileExtMapping } = this.props; 8 | return ( 9 |
10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | { 21 | Object.keys(fileExt2Env).map((k, idx) => 22 | 23 | 24 | 25 | 28 | 29 | ) 30 | } 31 | 32 |
{k}{fileExt2Env[k]} 26 | onRemoveFileExtMapping(k)} className="btn btn-danger"> 27 |
33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | FileExt2EnvForm.propTypes = { 57 | fileExt2Env: PropTypes.object, 58 | draftMapping: PropTypes.object, 59 | onRemoveFileExtMapping: PropTypes.func.isRequired, 60 | onDraftMappingChange: PropTypes.func.isRequired, 61 | onDraftMappingCreate: PropTypes.func.isRequired, 62 | environments: PropTypes.oneOfType([ 63 | PropTypes.object, 64 | PropTypes.array, 65 | ]), 66 | }; 67 | 68 | export default FileExt2EnvForm; 69 | -------------------------------------------------------------------------------- /app/components/FunctionLogItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * FunctionLogItem 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | 9 | function FunctionLogItem({ log }) { 10 | if (log === null || log.log.length === 0 || !log.log.trim()) { 11 | return
; 12 | } 13 | const type = log.stream === 'stdout' ? 'alert-success' : 'alert-warning'; 14 | return ( 15 |
16 | {log.time.toISOString()} 17 | {`Version-${log.funcuid.substring(0, 7)}`} 18 | {`Container-${log.container.substring(0, 7)}`} 19 | {log.stream} 20 |
21 | {log.log} 22 |
23 | ); 24 | } 25 | 26 | FunctionLogItem.propTypes = { 27 | log: PropTypes.object, 28 | }; 29 | 30 | export default FunctionLogItem; 31 | -------------------------------------------------------------------------------- /app/components/FunctionTabForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * FunctionForm Messages 3 | * 4 | * This contains all the text for the FunctionForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.FunctionForm.header', 11 | defaultMessage: 'This is the FunctionTabForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/FunctionTestForm/historyItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * FunctionTestHistoryItemForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | class historyItem extends React.Component { // eslint-disable-line react/prefer-stateless-function 10 | render() { 11 | const { index, item, onSelect } = this.props; 12 | const { method, headers, params, bodytype } = item; 13 | return ( 14 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | historyItem.propTypes = { 29 | index: React.PropTypes.number, 30 | item: React.PropTypes.object, 31 | onSelect: React.PropTypes.func.isRequired, 32 | }; 33 | 34 | export default historyItem; 35 | -------------------------------------------------------------------------------- /app/components/FunctionTestForm/requestBodyBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * RequestBodyBuilder 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import AceEditor from 'react-ace'; 9 | import 'brace/mode/json'; 10 | import 'brace/mode/xml'; 11 | import 'brace/mode/plain_text'; 12 | import 'brace/theme/monokai'; 13 | 14 | class RequestBodyBuilder extends React.Component { // eslint-disable-line react/prefer-stateless-function 15 | render() { 16 | const { bodytype, content, onSelectType, onContentChange } = this.props; 17 | return ( 18 |
19 | 24 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | RequestBodyBuilder.propTypes = { 40 | bodytype: React.PropTypes.string.isRequired, 41 | content: React.PropTypes.string.isRequired, 42 | onSelectType: React.PropTypes.func.isRequired, 43 | onContentChange: React.PropTypes.func.isRequired, 44 | }; 45 | 46 | export default RequestBodyBuilder; 47 | -------------------------------------------------------------------------------- /app/components/FunctionsList/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * FunctionsList 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import LoadingIndicator from 'components/LoadingIndicator'; 9 | import ErrorIndicator from 'components/ErrorIndicator'; 10 | import FunctionListItem from 'containers/FunctionListItem'; 11 | // import styled from 'styled-components'; 12 | import { FormattedMessage } from 'react-intl'; 13 | import commonMessages from 'messages'; 14 | 15 | function FunctionsList({ loading, error, items, onRemove, onChangeSortField }) { 16 | if (loading) { 17 | return ; 18 | } 19 | if (error !== false) { 20 | return ; 21 | } 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | { 34 | items.map((item, index) => ( 35 | onRemove(item)} 38 | /> 39 | )) 40 | } 41 | 42 |
onChangeSortField('name')}> onChangeSortField('environment')}>
43 | ); 44 | } 45 | 46 | FunctionsList.propTypes = { 47 | loading: PropTypes.bool, 48 | error: PropTypes.any, 49 | items: PropTypes.oneOfType([ 50 | PropTypes.object, 51 | PropTypes.array, 52 | ]), 53 | onRemove: PropTypes.func, 54 | onChangeSortField: PropTypes.func, 55 | }; 56 | 57 | export default FunctionsList; 58 | -------------------------------------------------------------------------------- /app/components/KeyValueBuilder/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * KeyValueBuilder 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Row from './row'; 9 | 10 | class KeyValueBuilder extends React.Component { // eslint-disable-line react/prefer-stateless-function 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | pairs: this.extractData(props.defaultValue), 15 | }; 16 | this.onChange = this.onChange.bind(this); 17 | this.onDelete = this.onDelete.bind(this); 18 | this.createNewRow = this.createNewRow.bind(this); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | const pairs = this.extractData(nextProps.defaultValue); 23 | this.setState({ pairs }); 24 | } 25 | 26 | onChange(index, pair) { 27 | const { pairs } = this.state; 28 | pairs[index] = pair; 29 | const dict = {}; 30 | pairs.map((p) => { dict[p[0]] = p[1]; return p; }); 31 | this.props.onChange({ 32 | target: { 33 | name: this.props.name, 34 | value: dict, 35 | }, 36 | }); 37 | } 38 | 39 | onDelete(index) { 40 | const { pairs } = this.state; 41 | pairs.splice(index, 1); 42 | this.setState({ pairs }); 43 | } 44 | 45 | extractData(defaultValue) { 46 | let pairs = [['', '']]; 47 | if (Object.keys(defaultValue).length > 0) { 48 | pairs = Object.keys(defaultValue).map((key) => [key, defaultValue[key]]); 49 | } 50 | return pairs; 51 | } 52 | 53 | createNewRow() { 54 | const { pairs } = this.state; 55 | pairs.push(['', '']); 56 | this.setState({ pairs }); 57 | } 58 | 59 | render() { 60 | const { pairs } = this.state; 61 | return ( 62 |
63 | {pairs.map((pair, index) => )} 64 | + 65 |
66 | ); 67 | } 68 | } 69 | 70 | KeyValueBuilder.propTypes = { 71 | name: React.PropTypes.string.isRequired, 72 | defaultValue: React.PropTypes.object.isRequired, 73 | onChange: React.PropTypes.func.isRequired, 74 | }; 75 | export default KeyValueBuilder; 76 | -------------------------------------------------------------------------------- /app/components/KeyValueBuilder/row.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * KeyValueRow 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | class Row extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.onChange = this.onChange.bind(this); 13 | } 14 | 15 | onChange(e) { 16 | const pair = [this.props.thekey, this.props.thevalue]; 17 | pair[e.target.name] = e.target.value; 18 | this.props.onChange(this.props.index, pair); 19 | } 20 | 21 | render() { 22 | const { index, thekey, thevalue } = this.props; 23 | return ( 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | Row.propTypes = { 40 | index: React.PropTypes.number.isRequired, 41 | thekey: React.PropTypes.string.isRequired, 42 | thevalue: React.PropTypes.string.isRequired, 43 | onChange: React.PropTypes.func.isRequired, 44 | onDelete: React.PropTypes.func.isRequired, 45 | }; 46 | 47 | export default Row; 48 | -------------------------------------------------------------------------------- /app/components/KubeWatcherCreateForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * KubeWatcherCreateForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | 12 | class KubeWatcherCreateForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | namespace: 'default', 18 | objtype: 'pod', 19 | labelselector: '', 20 | }; 21 | this.onChange = this.onChange.bind(this); 22 | this.onWatcherCreate = this.onWatcherCreate.bind(this); 23 | } 24 | 25 | onChange(event) { 26 | const target = event.target; 27 | this.state[target.name] = target.value; 28 | } 29 | 30 | onWatcherCreate(event) { 31 | event.preventDefault(); 32 | const { onCreate } = this.props; 33 | onCreate(this.state); 34 | } 35 | 36 | render() { 37 | const { namespace, objtype, labelselector } = this.state; 38 | return ( 39 |
40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | KubeWatcherCreateForm.propTypes = { 61 | onCreate: React.PropTypes.func, 62 | }; 63 | 64 | export default KubeWatcherCreateForm; 65 | -------------------------------------------------------------------------------- /app/components/KubeWatcherCreateForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * KubeWatcherCreateForm Messages 3 | * 4 | * This contains all the text for the KubeWatcherCreateForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.KubeWatcherCreateForm.header', 11 | defaultMessage: 'This is the KubeWatcherCreateForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/KubeWatcherForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * KubeWatcherForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import KubeWatcherCreateForm from 'components/KubeWatcherCreateForm'; 10 | 11 | import { FormattedMessage } from 'react-intl'; 12 | import commonMessages from 'messages'; 13 | import messages from './messages'; 14 | import Item from './item'; 15 | 16 | class KubeWatcherForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | render() { 18 | const { watchers, onRemove, onCreate } = this.props; 19 | return ( 20 |
21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | { 33 | watchers.map((item, index) => ( 34 | { onRemove(item); }} /> 35 | )) 36 | } 37 | 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | KubeWatcherForm.propTypes = { 46 | watchers: React.PropTypes.array, 47 | onRemove: React.PropTypes.func, 48 | onCreate: React.PropTypes.func, 49 | }; 50 | 51 | export default KubeWatcherForm; 52 | -------------------------------------------------------------------------------- /app/components/KubeWatcherForm/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * KubeWatcherItemForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | 12 | class Item extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | const { watcher, onRemove } = this.props; 15 | return ( 16 | 17 | {watcher.namespace} 18 | {watcher.objtype} 19 | {watcher.labelselector} 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | Item.propTypes = { 27 | watcher: React.PropTypes.object, 28 | onRemove: React.PropTypes.func, 29 | }; 30 | 31 | export default Item; 32 | -------------------------------------------------------------------------------- /app/components/KubeWatcherForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * KubeWatcherForm Messages 3 | * 4 | * This contains all the text for the KubeWatcherForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | headerkubewatcher: { 10 | id: 'app.components.KubeWatcherForm.headerkubewatcher', 11 | defaultMessage: 'Kube Watcher', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/Circle.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | const circleFadeDelay = keyframes` 5 | 0%, 6 | 39%, 7 | 100% { 8 | opacity: 0; 9 | } 10 | 11 | 40% { 12 | opacity: 1; 13 | } 14 | `; 15 | 16 | const Circle = (props) => { 17 | const CirclePrimitive = styled.div` 18 | width: 100%; 19 | height: 100%; 20 | position: absolute; 21 | left: 0; 22 | top: 0; 23 | ${props.rotate && ` 24 | -webkit-transform: rotate(${props.rotate}deg); 25 | -ms-transform: rotate(${props.rotate}deg); 26 | transform: rotate(${props.rotate}deg); 27 | `} 28 | 29 | &:before { 30 | content: ''; 31 | display: block; 32 | margin: 0 auto; 33 | width: 15%; 34 | height: 15%; 35 | background-color: #999; 36 | border-radius: 100%; 37 | animation: ${circleFadeDelay} 1.2s infinite ease-in-out both; 38 | ${props.delay && ` 39 | -webkit-animation-delay: ${props.delay}s; 40 | animation-delay: ${props.delay}s; 41 | `} 42 | } 43 | `; 44 | return ; 45 | }; 46 | 47 | Circle.propTypes = { 48 | delay: PropTypes.number, 49 | rotate: PropTypes.number, 50 | }; 51 | 52 | export default Circle; 53 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | margin: 2em auto; 5 | width: 40px; 6 | height: 40px; 7 | position: relative; 8 | `; 9 | 10 | export default Wrapper; 11 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Circle from './Circle'; 4 | import Wrapper from './Wrapper'; 5 | 6 | const LoadingIndicator = () => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default LoadingIndicator; 24 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/tests/Circle.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import Circle from '../Circle'; 5 | 6 | describe('', () => { 7 | it('should render an
tag', () => { 8 | const renderedComponent = mount(); 9 | expect(renderedComponent.find('div').length).toEqual(1); 10 | }); 11 | 12 | it('should have a className attribute', () => { 13 | const renderedComponent = mount(); 14 | expect(renderedComponent.find('div').prop('className')).toBeDefined(); 15 | }); 16 | 17 | it('should not adopt attributes', () => { 18 | const id = 'test'; 19 | const renderedComponent = mount(); 20 | expect(renderedComponent.find('div').prop('id')).toBeUndefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/tests/Wrapper.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Wrapper from '../Wrapper'; 5 | 6 | describe('', () => { 7 | it('should render an
tag', () => { 8 | const renderedComponent = shallow(); 9 | expect(renderedComponent.type()).toEqual('div'); 10 | }); 11 | 12 | it('should have a className attribute', () => { 13 | const renderedComponent = shallow(); 14 | expect(renderedComponent.prop('className')).toBeDefined(); 15 | }); 16 | 17 | it('should adopt a valid attribute', () => { 18 | const id = 'test'; 19 | const renderedComponent = shallow(); 20 | expect(renderedComponent.prop('id')).toEqual(id); 21 | }); 22 | 23 | it('should not adopt an invalid attribute', () => { 24 | const renderedComponent = shallow(); 25 | expect(renderedComponent.prop('attribute')).toBeUndefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'enzyme'; 3 | 4 | import LoadingIndicator from '../index'; 5 | 6 | describe('', () => { 7 | it('should render 13 divs', () => { 8 | const renderedComponent = render( 9 | 10 | ); 11 | expect(renderedComponent.find('div').length).toBe(13); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/NavLink/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * NavLink 4 | * 5 | */ 6 | 7 | import React, { PureComponent } from 'react'; 8 | import { Link } from 'react-router'; 9 | 10 | class NavLink extends PureComponent { // eslint-disable-line react/prefer-stateless-function 11 | render() { 12 | const isActive = this.context.router.isActive(this.props.to, false); 13 | const className = isActive ? 'active' : ''; 14 | return ( 15 |
  • 16 | 17 |
  • 18 | ); 19 | } 20 | } 21 | 22 | NavLink.contextTypes = { 23 | router: React.PropTypes.object, 24 | }; 25 | NavLink.propTypes = { 26 | to: React.PropTypes.string, 27 | }; 28 | export default NavLink; 29 | -------------------------------------------------------------------------------- /app/components/ProgressBar/Percent.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export default styled.div` 4 | height: 2px; 5 | background: #29D; 6 | transition: all 300ms ease; 7 | `; 8 | -------------------------------------------------------------------------------- /app/components/ProgressBar/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export default styled.div` 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | visibility: ${(props) => props.hidden ? 'hidden' : 'visible'}; 9 | opacity: ${(props) => props.hidden ? '0' : '1'}; 10 | transition: all 500ms ease-in-out; 11 | z-index: ${(props) => props.hidden ? '-10' : '9999'}; 12 | `; 13 | -------------------------------------------------------------------------------- /app/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProgressBar from './ProgressBar'; 3 | 4 | function withProgressBar(WrappedComponent) { 5 | class AppWithProgressBar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | progress: -1, 10 | loadedRoutes: props.location && [props.location.pathname], 11 | }; 12 | this.updateProgress = this.updateProgress.bind(this); 13 | } 14 | 15 | componentWillMount() { 16 | // Store a reference to the listener. 17 | /* istanbul ignore next */ 18 | this.unsubscribeHistory = this.props.router && this.props.router.listenBefore((location) => { 19 | // Do not show progress bar for already loaded routes. 20 | if (this.state.loadedRoutes.indexOf(location.pathname) === -1) { 21 | this.updateProgress(0); 22 | } 23 | }); 24 | } 25 | 26 | componentWillUpdate(newProps, newState) { 27 | const { loadedRoutes, progress } = this.state; 28 | const { pathname } = newProps.location; 29 | 30 | // Complete progress when route changes. But prevent state update while re-rendering. 31 | if (loadedRoutes.indexOf(pathname) === -1 && progress !== -1 && newState.progress < 100) { 32 | this.updateProgress(100); 33 | this.setState({ 34 | loadedRoutes: loadedRoutes.concat([pathname]), 35 | }); 36 | } 37 | } 38 | 39 | componentWillUnmount() { 40 | // Unset unsubscribeHistory since it won't be garbage-collected. 41 | this.unsubscribeHistory = undefined; 42 | } 43 | 44 | updateProgress(progress) { 45 | this.setState({ progress }); 46 | } 47 | 48 | render() { 49 | return ( 50 |
    51 | 52 | 53 |
    54 | ); 55 | } 56 | } 57 | 58 | AppWithProgressBar.propTypes = { 59 | location: React.PropTypes.object, 60 | router: React.PropTypes.object, 61 | }; 62 | 63 | return AppWithProgressBar; 64 | } 65 | 66 | export default withProgressBar; 67 | -------------------------------------------------------------------------------- /app/components/ProgressBar/tests/Percent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Percent from '../Percent'; 5 | 6 | it('should render an
    tag', () => { 7 | const renderedComponent = shallow(); 8 | expect(renderedComponent.type()).toEqual('div'); 9 | }); 10 | -------------------------------------------------------------------------------- /app/components/ProgressBar/tests/Wrapper.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Wrapper from '../Wrapper'; 5 | 6 | it('should render an
    tag', () => { 7 | const renderedComponent = shallow(); 8 | expect(renderedComponent.type()).toEqual('div'); 9 | }); 10 | -------------------------------------------------------------------------------- /app/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LocaleToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | import Option from './option'; 10 | 11 | function Toggle(props) { 12 | let content = (); 13 | 14 | // If we have items, render them 15 | if (props.values) { 16 | content = props.values.map((value) => ( 17 | 14 | ); 15 | 16 | Option.propTypes = { 17 | value: React.PropTypes.string.isRequired, 18 | message: React.PropTypes.object, 19 | intl: intlShape.isRequired, 20 | }; 21 | 22 | export default injectIntl(Option); 23 | -------------------------------------------------------------------------------- /app/components/Toggle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { IntlProvider, defineMessages } from 'react-intl'; 4 | 5 | import Toggle from '../index'; 6 | 7 | describe('', () => { 8 | it('should contain default text', () => { 9 | const defaultEnMessage = 'someContent'; 10 | const defaultDeMessage = 'someOtherContent'; 11 | const messages = defineMessages({ 12 | en: { 13 | id: 'boilerplate.containers.LocaleToggle.en', 14 | defaultMessage: defaultEnMessage, 15 | }, 16 | de: { 17 | id: 'boilerplate.containers.LocaleToggle.en', 18 | defaultMessage: defaultDeMessage, 19 | }, 20 | }); 21 | const renderedComponent = shallow( 22 | 23 | 24 | 25 | ); 26 | expect(renderedComponent.contains()).toBe(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /app/components/TriggerHttpCreateForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerHttpCreateForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | 12 | class TriggerHttpCreateForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | urlpattern: '/', 18 | method: 'GET', 19 | }; 20 | this.onChange = this.onChange.bind(this); 21 | this.onTriggerCreate = this.onTriggerCreate.bind(this); 22 | } 23 | 24 | onChange(event) { 25 | const target = event.target; 26 | this.state[target.name] = target.value; 27 | } 28 | 29 | onTriggerCreate(event) { 30 | event.preventDefault(); 31 | const { onCreate } = this.props; 32 | onCreate(this.state); 33 | } 34 | 35 | render() { 36 | return ( 37 |
    38 |
    39 | 40 | 46 |
    47 | 48 |
    49 | 50 | 51 |
    52 | 53 |
    54 | ); 55 | } 56 | } 57 | 58 | TriggerHttpCreateForm.propTypes = { 59 | onCreate: React.PropTypes.func, 60 | }; 61 | 62 | export default TriggerHttpCreateForm; 63 | -------------------------------------------------------------------------------- /app/components/TriggerHttpCreateForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerHttpCreateForm Messages 3 | * 4 | * This contains all the text for the TriggerHttpCreateForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.TriggerHttpCreateForm.header', 11 | defaultMessage: 'This is the TriggerHttpCreateForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/TriggerHttpForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerHttpForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import TriggerHttpCreateForm from 'components/TriggerHttpCreateForm'; 10 | import { FormattedMessage } from 'react-intl'; 11 | import commonMessages from 'messages'; 12 | import messages from './messages'; 13 | import Item from './item'; 14 | 15 | class TriggerHttpForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | const { triggers, onRemove, onCreate } = this.props; 18 | return ( 19 |
    20 |

    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | { 31 | triggers.map((item, index) => ( 32 | { onRemove(item); }} /> 33 | )) 34 | } 35 | 36 |
    37 | 38 |
    39 | ); 40 | } 41 | } 42 | 43 | TriggerHttpForm.propTypes = { 44 | triggers: React.PropTypes.array, 45 | onRemove: React.PropTypes.func, 46 | onCreate: React.PropTypes.func, 47 | }; 48 | 49 | export default TriggerHttpForm; 50 | -------------------------------------------------------------------------------- /app/components/TriggerHttpForm/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerHttpItemForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import ReactTooltip from 'react-tooltip'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | // import styled from 'styled-components'; 12 | 13 | 14 | class Item extends React.Component { // eslint-disable-line react/prefer-stateless-function 15 | 16 | makeCopy(endpoint) { 17 | return () => { 18 | window.prompt('Copy to clipboard: Ctrl+C, Enter', endpoint); 19 | }; 20 | } 21 | 22 | render() { 23 | const { trigger, onRemove } = this.props; 24 | const endpoint = `${window.location.origin}/proxy/router${trigger.urlpattern}`; 25 | return ( 26 | 27 | {trigger.method} 28 | {trigger.urlpattern} 29 | 30 | { ' ' } 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | Item.propTypes = { 40 | trigger: React.PropTypes.object, 41 | onRemove: React.PropTypes.func, 42 | }; 43 | 44 | export default Item; 45 | -------------------------------------------------------------------------------- /app/components/TriggerHttpForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerHttpForm Messages 3 | * 4 | * This contains all the text for the TriggerHttpForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | headerhttptrigger: { 10 | id: 'app.components.TriggerHttpForm.headerhttptrigger', 11 | defaultMessage: 'Http trigger', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/TriggerMQCreateForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerMQCreateForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | 12 | class TriggerMQCreateForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | messageQueueType: 'nats-streaming', 18 | topic: '', 19 | respTopic: '', 20 | }; 21 | this.onChange = this.onChange.bind(this); 22 | this.onTriggerCreate = this.onTriggerCreate.bind(this); 23 | } 24 | 25 | onChange(event) { 26 | const target = event.target; 27 | this.state[target.name] = target.value; 28 | } 29 | 30 | onTriggerCreate(event) { 31 | event.preventDefault(); 32 | const { onCreate } = this.props; 33 | onCreate(this.state); 34 | } 35 | 36 | render() { 37 | return ( 38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 | 45 | 46 |
    47 |
    48 | 49 | 50 |
    51 | 52 |
    53 | ); 54 | } 55 | } 56 | 57 | TriggerMQCreateForm.propTypes = { 58 | onCreate: React.PropTypes.func, 59 | }; 60 | 61 | export default TriggerMQCreateForm; 62 | -------------------------------------------------------------------------------- /app/components/TriggerMQCreateForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerMQCreateForm Messages 3 | * 4 | * This contains all the text for the TriggerMQCreateForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.TriggerMQCreateForm.header', 11 | defaultMessage: 'This is the TriggerMQCreateForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/TriggerMQForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerMQForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import TriggerMQCreateForm from 'components/TriggerMQCreateForm'; 10 | import { FormattedMessage } from 'react-intl'; 11 | import commonMessages from 'messages'; 12 | import messages from './messages'; 13 | import Item from './item'; 14 | 15 | class TriggerMQForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | const { triggers, onRemove, onCreate } = this.props; 18 | return ( 19 |
    20 |

    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | { 32 | triggers.map((item, index) => ( 33 | { onRemove(item); }} /> 34 | )) 35 | } 36 | 37 |
    38 | 39 |
    40 | ); 41 | } 42 | } 43 | 44 | TriggerMQForm.propTypes = { 45 | triggers: React.PropTypes.array, 46 | onRemove: React.PropTypes.func, 47 | onCreate: React.PropTypes.func, 48 | }; 49 | 50 | export default TriggerMQForm; 51 | -------------------------------------------------------------------------------- /app/components/TriggerMQForm/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerHttpItemForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import commonMessages from 'messages'; 10 | // import styled from 'styled-components'; 11 | 12 | 13 | class Item extends React.Component { // eslint-disable-line react/prefer-stateless-function 14 | 15 | render() { 16 | const { trigger, onRemove } = this.props; 17 | return ( 18 | 19 | {trigger.messageQueueType} 20 | {trigger.topic} 21 | {trigger.respTopic} 22 | 23 | { ' ' } 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | Item.propTypes = { 31 | trigger: React.PropTypes.object, 32 | onRemove: React.PropTypes.func, 33 | }; 34 | 35 | export default Item; 36 | -------------------------------------------------------------------------------- /app/components/TriggerMQForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerMQForm Messages 3 | * 4 | * This contains all the text for the TriggerMQForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | headerhttptrigger: { 10 | id: 'app.components.TriggerMQForm.headerhttptrigger', 11 | defaultMessage: 'MQ trigger', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/TriggerTimerCreateForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerTimerCreateForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import commonMessages from 'messages'; 11 | 12 | class TriggerTimerCreateForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | cron: '', 18 | description: '', 19 | }; 20 | this.onChange = this.onChange.bind(this); 21 | this.onTriggerCreate = this.onTriggerCreate.bind(this); 22 | } 23 | 24 | onChange(event) { 25 | const target = event.target; 26 | this.state[target.name] = target.value; 27 | } 28 | 29 | onTriggerCreate(event) { 30 | event.preventDefault(); 31 | const { onCreate } = this.props; 32 | onCreate(this.state); 33 | } 34 | 35 | render() { 36 | return ( 37 |
    38 |
    39 | 40 | 41 |
    42 | 43 |
    44 | ); 45 | } 46 | } 47 | 48 | TriggerTimerCreateForm.propTypes = { 49 | onCreate: React.PropTypes.func, 50 | }; 51 | 52 | export default TriggerTimerCreateForm; 53 | -------------------------------------------------------------------------------- /app/components/TriggerTimerCreateForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerTimerCreateForm Messages 3 | * 4 | * This contains all the text for the TriggerTimerCreateForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.TriggerTimerCreateForm.header', 11 | defaultMessage: 'This is the TriggerTimerCreateForm component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/TriggerTimerForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerTimerForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | import TriggerTimerCreateForm from 'components/TriggerTimerCreateForm'; 10 | import { FormattedMessage } from 'react-intl'; 11 | import commonMessages from 'messages'; 12 | import messages from './messages'; 13 | import Item from './item'; 14 | 15 | class TriggerTimerForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | const { triggers, onRemove, onCreate } = this.props; 18 | return ( 19 |
    20 |

    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | { 30 | triggers.map((item, index) => ( 31 | { onRemove(item); }} /> 32 | )) 33 | } 34 | 35 |
    36 | 37 |
    38 | ); 39 | } 40 | } 41 | 42 | TriggerTimerForm.propTypes = { 43 | triggers: React.PropTypes.array, 44 | onRemove: React.PropTypes.func, 45 | onCreate: React.PropTypes.func, 46 | }; 47 | 48 | export default TriggerTimerForm; 49 | -------------------------------------------------------------------------------- /app/components/TriggerTimerForm/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TriggerHttpItemForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import commonMessages from 'messages'; 10 | // import styled from 'styled-components'; 11 | 12 | 13 | class Item extends React.Component { // eslint-disable-line react/prefer-stateless-function 14 | 15 | render() { 16 | const { trigger, onRemove } = this.props; 17 | return ( 18 | 19 | {trigger.cron} 20 | 21 | { ' ' } 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | Item.propTypes = { 29 | trigger: React.PropTypes.object, 30 | onRemove: React.PropTypes.func, 31 | }; 32 | 33 | export default Item; 34 | -------------------------------------------------------------------------------- /app/components/TriggerTimerForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TriggerTimerForm Messages 3 | * 4 | * This contains all the text for the TriggerTimerForm component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | headerhttptrigger: { 10 | id: 'app.components.TriggerTimerForm.headerhttptrigger', 11 | defaultMessage: 'Timer trigger', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | export const DEFAULT_LOCALE = 'en'; 13 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a necessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | import { browserHistory } from 'react-router'; 16 | import withProgressBar from 'components/ProgressBar'; 17 | import LocaleToggle from 'containers/LocaleToggle'; 18 | import { FormattedMessage } from 'react-intl'; 19 | import { Navbar, Nav, NavItem } from 'react-bootstrap'; 20 | import commonMessages from 'messages'; 21 | 22 | export function App(props) { 23 | const onLink = (e) => { 24 | browserHistory.push(e); 25 | }; 26 | 27 | return ( 28 |
    29 | 30 | 31 | 32 | Fission UI 33 | 34 | 35 | 36 | 37 | 51 | 52 | 53 |
    54 | {React.Children.toArray(props.children)} 55 |
    56 | 57 |
    58 | ); 59 | } 60 | App.propTypes = { 61 | children: React.PropTypes.node, 62 | }; 63 | 64 | export default withProgressBar(App); 65 | -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | // makeSelectLocationState expects a plain JS object for the routing state 2 | const makeSelectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | makeSelectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/App/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import App from '../index'; 5 | 6 | describe('', () => { 7 | it('should render its children', () => { 8 | const children = (

    Test

    ); 9 | const renderedComponent = shallow( 10 | 11 | {children} 12 | 13 | ); 14 | expect(renderedComponent.contains(children)).toBe(true); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { makeSelectLocationState } from 'containers/App/selectors'; 4 | 5 | describe('makeSelectLocationState', () => { 6 | it('should select the route as a plain JS object', () => { 7 | const route = fromJS({ 8 | locationBeforeTransitions: null, 9 | }); 10 | const mockedState = fromJS({ 11 | route, 12 | }); 13 | expect(makeSelectLocationState()(mockedState)).toEqual(route.toJS()); 14 | }); 15 | 16 | it('should return cached js routeState for same concurrent calls', () => { 17 | const route = fromJS({ 18 | locationBeforeTransitions: null, 19 | }); 20 | const mockedState = fromJS({ 21 | route, 22 | }); 23 | const selectLocationState = makeSelectLocationState(); 24 | 25 | const firstRouteStateJS = selectLocationState(mockedState); 26 | expect(selectLocationState(mockedState)).toBe(firstRouteStateJS); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigListItem/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkConfigListItem 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import { Link } from 'react-router'; 10 | import commonMessages from 'messages'; 11 | 12 | export class BenchmarkConfigListItem extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | const { item, onRemove } = this.props; 15 | const { name } = item.metadata; 16 | return ( 17 | 18 | { name } 19 | 20 | { ' ' } 21 | { ' ' } 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | BenchmarkConfigListItem.propTypes = { 30 | item: PropTypes.object, 31 | onRemove: PropTypes.func, 32 | }; 33 | 34 | export default BenchmarkConfigListItem; 35 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarksListPage actions 4 | * 5 | */ 6 | 7 | import { 8 | UPDATE_CONFIG_REQUEST, 9 | GET_CONFIG_REQUEST, 10 | CREATE_CONFIG_REQUEST, 11 | } from 'containers/BenchmarksPage/constants'; 12 | 13 | export function updateConfigAction(config) { 14 | return { 15 | type: UPDATE_CONFIG_REQUEST, 16 | config, 17 | }; 18 | } 19 | export function createConfigAction(config) { 20 | return { 21 | type: CREATE_CONFIG_REQUEST, 22 | config, 23 | }; 24 | } 25 | export function getConfigAction(name) { 26 | return { 27 | type: GET_CONFIG_REQUEST, 28 | name, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarksListPage Messages 3 | * 4 | * This contains all the text for the BenchmarksListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.BenchmarksListPage.header', 11 | defaultMessage: 'This is BenchmarksListPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { createBenchmarkConfig, updateBenchmarkConfig, getBenchmarkConfig } from 'utils/tprapi'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | import { 5 | CREATE_CONFIG_REQUEST, 6 | CREATE_CONFIG_SUCCESS, 7 | CREATE_CONFIG_ERROR, 8 | GET_CONFIG_REQUEST, 9 | GET_CONFIG_SUCCESS, 10 | GET_CONFIG_ERROR, 11 | UPDATE_CONFIG_REQUEST, 12 | UPDATE_CONFIG_SUCCESS, 13 | UPDATE_CONFIG_ERROR, 14 | } from 'containers/BenchmarksPage/constants'; 15 | 16 | function* createConfig(action) { 17 | try { 18 | const data = yield call(createBenchmarkConfig, action.config); 19 | yield put({ type: CREATE_CONFIG_SUCCESS, data }); 20 | } catch (error) { 21 | yield put({ type: CREATE_CONFIG_ERROR, error }); 22 | } 23 | } 24 | 25 | function* updateConfig(action) { 26 | try { 27 | const data = yield call(updateBenchmarkConfig, action.config); 28 | yield put({ type: UPDATE_CONFIG_SUCCESS, data }); 29 | } catch (error) { 30 | yield put({ type: UPDATE_CONFIG_ERROR, error }); 31 | } 32 | } 33 | 34 | function* getConfig(action) { 35 | try { 36 | const data = yield call(getBenchmarkConfig, action.name); 37 | yield put({ type: GET_CONFIG_SUCCESS, data }); 38 | } catch (error) { 39 | yield put({ type: GET_CONFIG_ERROR, error }); 40 | } 41 | } 42 | 43 | function makeSaga(l, f) { 44 | return function* s() { 45 | const watcher = yield takeLatest(l, f); 46 | 47 | // Suspend execution until location changes 48 | yield take(LOCATION_CHANGE); 49 | yield cancel(watcher); 50 | }; 51 | } 52 | 53 | // All sagas to be loaded 54 | export default [ 55 | [CREATE_CONFIG_REQUEST, createConfig], 56 | [UPDATE_CONFIG_REQUEST, updateConfig], 57 | [GET_CONFIG_REQUEST, getConfig], 58 | ].map((d) => makeSaga(d[0], d[1])); 59 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigsListPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarksListPage actions 4 | * 5 | */ 6 | 7 | import { 8 | LOAD_CONFIGS_REQUEST, 9 | UPDATE_CONFIG_REQUEST, 10 | GET_CONFIG_REQUEST, 11 | CREATE_CONFIG_REQUEST, 12 | DELETE_CONFIG_REQUEST, 13 | } from 'containers/BenchmarksPage/constants'; 14 | 15 | export function removeConfigAction(config) { 16 | return { 17 | type: DELETE_CONFIG_REQUEST, 18 | config, 19 | }; 20 | } 21 | export function updateConfigAction(config) { 22 | return { 23 | type: UPDATE_CONFIG_REQUEST, 24 | config, 25 | }; 26 | } 27 | export function createConfigAction(config) { 28 | return { 29 | type: CREATE_CONFIG_REQUEST, 30 | config, 31 | }; 32 | } 33 | export function getConfigAction(name) { 34 | return { 35 | type: GET_CONFIG_REQUEST, 36 | name, 37 | }; 38 | } 39 | export function loadConfigsAction() { 40 | return { 41 | type: LOAD_CONFIGS_REQUEST, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigsListPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarksListPage Messages 3 | * 4 | * This contains all the text for the BenchmarksListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.BenchmarksListPage.header', 11 | defaultMessage: 'This is BenchmarksListPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/BenchmarkConfigsListPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { 3 | getBenchmarkConfigs, 4 | removeBenchmarkConfig, 5 | createBenchmarkConfig, 6 | updateBenchmarkConfig, 7 | getBenchmarkConfig, 8 | } from 'utils/tprapi'; 9 | import { LOCATION_CHANGE } from 'react-router-redux'; 10 | import { 11 | LOAD_CONFIGS_REQUEST, 12 | LOAD_CONFIGS_SUCCESS, 13 | LOAD_CONFIGS_ERROR, 14 | DELETE_CONFIG_REQUEST, 15 | DELETE_CONFIG_SUCCESS, 16 | DELETE_CONFIG_ERROR, 17 | CREATE_CONFIG_REQUEST, 18 | CREATE_CONFIG_SUCCESS, 19 | CREATE_CONFIG_ERROR, 20 | GET_CONFIG_REQUEST, 21 | GET_CONFIG_SUCCESS, 22 | GET_CONFIG_ERROR, 23 | UPDATE_CONFIG_REQUEST, 24 | UPDATE_CONFIG_SUCCESS, 25 | UPDATE_CONFIG_ERROR, 26 | } from 'containers/BenchmarksPage/constants'; 27 | 28 | function* loadConfigs() { 29 | try { 30 | const data = yield call(getBenchmarkConfigs); 31 | yield put({ type: LOAD_CONFIGS_SUCCESS, data: data.items }); 32 | } catch (error) { 33 | yield put({ type: LOAD_CONFIGS_ERROR, error }); 34 | } 35 | } 36 | 37 | function* deleteConfig(action) { 38 | try { 39 | yield call(removeBenchmarkConfig, action.config); 40 | yield put({ type: DELETE_CONFIG_SUCCESS, data: action.config }); 41 | } catch (error) { 42 | yield put({ type: DELETE_CONFIG_ERROR, error }); 43 | } 44 | } 45 | 46 | function* createConfig(action) { 47 | try { 48 | const data = yield call(createBenchmarkConfig, action.config); 49 | yield put({ type: CREATE_CONFIG_SUCCESS, data }); 50 | } catch (error) { 51 | yield put({ type: CREATE_CONFIG_ERROR, error }); 52 | } 53 | } 54 | 55 | function* updateConfig(action) { 56 | try { 57 | const data = yield call(updateBenchmarkConfig, action.config); 58 | yield put({ type: UPDATE_CONFIG_SUCCESS, data }); 59 | } catch (error) { 60 | yield put({ type: UPDATE_CONFIG_ERROR, error }); 61 | } 62 | } 63 | 64 | function* getConfig(action) { 65 | try { 66 | const data = yield call(getBenchmarkConfig, action.name); 67 | yield put({ type: GET_CONFIG_SUCCESS, data }); 68 | } catch (error) { 69 | yield put({ type: GET_CONFIG_ERROR, error }); 70 | } 71 | } 72 | 73 | function makeSaga(l, f) { 74 | return function* s() { 75 | const watcher = yield takeLatest(l, f); 76 | 77 | // Suspend execution until location changes 78 | yield take(LOCATION_CHANGE); 79 | yield cancel(watcher); 80 | }; 81 | } 82 | 83 | // All sagas to be loaded 84 | export default [ 85 | [LOAD_CONFIGS_REQUEST, loadConfigs], 86 | [DELETE_CONFIG_REQUEST, deleteConfig], 87 | [CREATE_CONFIG_REQUEST, createConfig], 88 | [UPDATE_CONFIG_REQUEST, updateConfig], 89 | [GET_CONFIG_REQUEST, getConfig], 90 | ].map((d) => makeSaga(d[0], d[1])); 91 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstanceListItem/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkConfigListItem 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import { Link } from 'react-router'; 10 | import commonMessages from 'messages'; 11 | 12 | export class BenchmarkConfigListItem extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | const { item, onRun, onStop, onRemove } = this.props; 15 | const { name, labels } = item.metadata; 16 | const { status } = item.spec; 17 | return ( 18 | 19 | { name } 20 | { status } 21 | 22 | 23 | 24 | { ' ' } 25 | { ' ' } 26 | { ' ' } 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | BenchmarkConfigListItem.propTypes = { 35 | item: PropTypes.object, 36 | onRemove: PropTypes.func, 37 | onRun: PropTypes.func, 38 | onStop: PropTypes.func, 39 | }; 40 | 41 | export default BenchmarkConfigListItem; 42 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarksListPage actions 4 | * 5 | */ 6 | 7 | import { 8 | GET_INSTANCE_REQUEST, 9 | GET_CONFIG_REQUEST, 10 | } from 'containers/BenchmarksPage/constants'; 11 | 12 | export function getInstanceAction(name) { 13 | return { 14 | type: GET_INSTANCE_REQUEST, 15 | name, 16 | }; 17 | } 18 | 19 | export function getConfigAction(name) { 20 | return { 21 | type: GET_CONFIG_REQUEST, 22 | name, 23 | }; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarksListPage Messages 3 | * 4 | * This contains all the text for the BenchmarksListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.BenchmarksListPage.header', 11 | defaultMessage: 'This is BenchmarksListPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancePage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { getBenchmarkInstance, getBenchmarkConfig } from 'utils/tprapi'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | import { 5 | GET_INSTANCE_REQUEST, 6 | GET_INSTANCE_SUCCESS, 7 | GET_INSTANCE_ERROR, 8 | GET_CONFIG_REQUEST, 9 | GET_CONFIG_SUCCESS, 10 | GET_CONFIG_ERROR, 11 | } from 'containers/BenchmarksPage/constants'; 12 | 13 | function* getInstance(action) { 14 | try { 15 | const data = yield call(getBenchmarkInstance, action.name); 16 | yield put({ type: GET_INSTANCE_SUCCESS, data }); 17 | } catch (error) { 18 | yield put({ type: GET_INSTANCE_ERROR, error }); 19 | } 20 | } 21 | 22 | function* getConfig(action) { 23 | try { 24 | const data = yield call(getBenchmarkConfig, action.name); 25 | yield put({ type: GET_CONFIG_SUCCESS, data }); 26 | } catch (error) { 27 | yield put({ type: GET_CONFIG_ERROR, error }); 28 | } 29 | } 30 | 31 | function makeSaga(l, f) { 32 | return function* s() { 33 | const watcher = yield takeLatest(l, f); 34 | 35 | // Suspend execution until location changes 36 | yield take(LOCATION_CHANGE); 37 | yield cancel(watcher); 38 | }; 39 | } 40 | 41 | // All sagas to be loaded 42 | export default [ 43 | [GET_INSTANCE_REQUEST, getInstance], 44 | [GET_CONFIG_REQUEST, getConfig], 45 | ].map((d) => makeSaga(d[0], d[1])); 46 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancesListPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkInstancesListPage actions 4 | * 5 | */ 6 | 7 | import { 8 | LOAD_INSTANCES_REQUEST, 9 | UPDATE_INSTANCE_REQUEST, 10 | GET_INSTANCE_REQUEST, 11 | CREATE_INSTANCE_REQUEST, 12 | DELETE_INSTANCE_REQUEST, 13 | } from 'containers/BenchmarksPage/constants'; 14 | 15 | export function removeInstanceAction(instance) { 16 | return { 17 | type: DELETE_INSTANCE_REQUEST, 18 | instance, 19 | }; 20 | } 21 | export function updateInstanceAction(ins, status) { 22 | const instance = Object.assign({}, ins); 23 | instance.spec.status = status; 24 | return { 25 | type: UPDATE_INSTANCE_REQUEST, 26 | instance, 27 | }; 28 | } 29 | export function createInstanceAction(instance) { 30 | return { 31 | type: CREATE_INSTANCE_REQUEST, 32 | instance, 33 | }; 34 | } 35 | export function getInstanceAction(name) { 36 | return { 37 | type: GET_INSTANCE_REQUEST, 38 | name, 39 | }; 40 | } 41 | export function loadInstancesAction() { 42 | return { 43 | type: LOAD_INSTANCES_REQUEST, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancesListPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarksListPage Messages 3 | * 4 | * This contains all the text for the BenchmarksListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.BenchmarksListPage.header', 11 | defaultMessage: 'This is BenchmarksListPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/BenchmarkInstancesListPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { 3 | getBenchmarkInstances, 4 | removeBenchmarkInstance, 5 | createBenchmarkInstance, 6 | updateBenchmarkInstance, 7 | getBenchmarkInstance, 8 | } from 'utils/tprapi'; 9 | import { LOCATION_CHANGE } from 'react-router-redux'; 10 | import { 11 | LOAD_INSTANCES_REQUEST, 12 | LOAD_INSTANCES_SUCCESS, 13 | LOAD_INSTANCES_ERROR, 14 | DELETE_INSTANCE_REQUEST, 15 | DELETE_INSTANCE_SUCCESS, 16 | DELETE_INSTANCE_ERROR, 17 | CREATE_INSTANCE_REQUEST, 18 | CREATE_INSTANCE_SUCCESS, 19 | CREATE_INSTANCE_ERROR, 20 | GET_INSTANCE_REQUEST, 21 | GET_INSTANCE_SUCCESS, 22 | GET_INSTANCE_ERROR, 23 | UPDATE_INSTANCE_REQUEST, 24 | UPDATE_INSTANCE_SUCCESS, 25 | UPDATE_INSTANCE_ERROR, 26 | } from 'containers/BenchmarksPage/constants'; 27 | 28 | function* loadInstances() { 29 | try { 30 | const data = yield call(getBenchmarkInstances); 31 | yield put({ type: LOAD_INSTANCES_SUCCESS, data: data.items }); 32 | } catch (error) { 33 | yield put({ type: LOAD_INSTANCES_ERROR, error }); 34 | } 35 | } 36 | 37 | function* deleteInstance(action) { 38 | try { 39 | yield call(removeBenchmarkInstance, action.instance); 40 | yield put({ type: DELETE_INSTANCE_SUCCESS, data: action.instance }); 41 | } catch (error) { 42 | yield put({ type: DELETE_INSTANCE_ERROR, error }); 43 | } 44 | } 45 | 46 | function* createInstance(action) { 47 | try { 48 | const data = yield call(createBenchmarkInstance, action.instance); 49 | yield put({ type: CREATE_INSTANCE_SUCCESS, data }); 50 | } catch (error) { 51 | yield put({ type: CREATE_INSTANCE_ERROR, error }); 52 | } 53 | } 54 | 55 | function* updateInstance(action) { 56 | try { 57 | const data = yield call(updateBenchmarkInstance, action.instance); 58 | yield put({ type: UPDATE_INSTANCE_SUCCESS, data }); 59 | } catch (error) { 60 | yield put({ type: UPDATE_INSTANCE_ERROR, error }); 61 | } 62 | } 63 | 64 | function* getInstance(action) { 65 | try { 66 | const data = yield call(getBenchmarkInstance, action.name); 67 | yield put({ type: GET_INSTANCE_SUCCESS, data }); 68 | } catch (error) { 69 | yield put({ type: GET_INSTANCE_ERROR, error }); 70 | } 71 | } 72 | 73 | function makeSaga(l, f) { 74 | return function* s() { 75 | const watcher = yield takeLatest(l, f); 76 | 77 | // Suspend execution until location changes 78 | yield take(LOCATION_CHANGE); 79 | yield cancel(watcher); 80 | }; 81 | } 82 | 83 | // All sagas to be loaded 84 | export default [ 85 | [LOAD_INSTANCES_REQUEST, loadInstances], 86 | [DELETE_INSTANCE_REQUEST, deleteInstance], 87 | [CREATE_INSTANCE_REQUEST, createInstance], 88 | [UPDATE_INSTANCE_REQUEST, updateInstance], 89 | [GET_INSTANCE_REQUEST, getInstance], 90 | ].map((d) => makeSaga(d[0], d[1])); 91 | -------------------------------------------------------------------------------- /app/containers/BenchmarksPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarksPage constants 4 | * 5 | */ 6 | 7 | export const LOAD_CONFIGS_REQUEST = 'app/Benchmarks/LOAD_CONFIGS_REQUEST'; 8 | export const LOAD_CONFIGS_SUCCESS = 'app/Benchmarks/LOAD_CONFIGS_SUCCESS'; 9 | export const LOAD_CONFIGS_ERROR = 'app/Benchmarks/LOAD_CONFIGS_ERROR'; 10 | export const CREATE_CONFIG_REQUEST = 'app/Benchmarks/CREATE_CONFIG_REQUEST'; 11 | export const CREATE_CONFIG_SUCCESS = 'app/Benchmarks/CREATE_CONFIG_SUCCESS'; 12 | export const CREATE_CONFIG_ERROR = 'app/Benchmarks/CREATE_CONFIG_ERROR'; 13 | export const GET_CONFIG_REQUEST = 'app/Benchmarks/GET_CONFIG_REQUEST'; 14 | export const GET_CONFIG_SUCCESS = 'app/Benchmarks/GET_CONFIG_SUCCESS'; 15 | export const GET_CONFIG_ERROR = 'app/Benchmarks/GET_CONFIG_ERROR'; 16 | export const DELETE_CONFIG_REQUEST = 'app/Benchmarks/DELETE_CONFIG_REQUEST'; 17 | export const DELETE_CONFIG_SUCCESS = 'app/Benchmarks/DELETE_CONFIG_SUCCESS'; 18 | export const DELETE_CONFIG_ERROR = 'app/Benchmarks/DELETE_CONFIG_ERROR'; 19 | export const UPDATE_CONFIG_REQUEST = 'app/Benchmarks/UPDATE_CONFIG_REQUEST'; 20 | export const UPDATE_CONFIG_SUCCESS = 'app/Benchmarks/UPDATE_CONFIG_SUCCESS'; 21 | export const UPDATE_CONFIG_ERROR = 'app/Benchmarks/UPDATE_CONFIG_ERROR'; 22 | 23 | export const LOAD_INSTANCES_REQUEST = 'app/Benchmarks/LOAD_INSTANCES_REQUEST'; 24 | export const LOAD_INSTANCES_SUCCESS = 'app/Benchmarks/LOAD_INSTANCES_SUCCESS'; 25 | export const LOAD_INSTANCES_ERROR = 'app/Benchmarks/LOAD_INSTANCES_ERROR'; 26 | export const CREATE_INSTANCE_REQUEST = 'app/Benchmarks/CREATE_INSTANCE_REQUEST'; 27 | export const CREATE_INSTANCE_SUCCESS = 'app/Benchmarks/CREATE_INSTANCE_SUCCESS'; 28 | export const CREATE_INSTANCE_ERROR = 'app/Benchmarks/CREATE_INSTANCE_ERROR'; 29 | export const GET_INSTANCE_REQUEST = 'app/Benchmarks/GET_INSTANCE_REQUEST'; 30 | export const GET_INSTANCE_SUCCESS = 'app/Benchmarks/GET_INSTANCE_SUCCESS'; 31 | export const GET_INSTANCE_ERROR = 'app/Benchmarks/GET_INSTANCE_ERROR'; 32 | export const DELETE_INSTANCE_REQUEST = 'app/Benchmarks/DELETE_INSTANCE_REQUEST'; 33 | export const DELETE_INSTANCE_SUCCESS = 'app/Benchmarks/DELETE_INSTANCE_SUCCESS'; 34 | export const DELETE_INSTANCE_ERROR = 'app/Benchmarks/DELETE_INSTANCE_ERROR'; 35 | export const UPDATE_INSTANCE_REQUEST = 'app/Benchmarks/UPDATE_INSTANCE_REQUEST'; 36 | export const UPDATE_INSTANCE_SUCCESS = 'app/Benchmarks/UPDATE_INSTANCE_SUCCESS'; 37 | export const UPDATE_INSTANCE_ERROR = 'app/Benchmarks/UPDATE_INSTANCE_ERROR'; 38 | -------------------------------------------------------------------------------- /app/containers/BenchmarksPage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarksPage 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import messages from './messages'; 10 | 11 | export class BenchmarksPage extends React.Component { // eslint-disable-line react/prefer-stateless-function 12 | render() { 13 | return ( 14 |
    15 |

    16 | 17 |

    18 | {React.Children.toArray(this.props.children)} 19 |
    20 | ); 21 | } 22 | } 23 | 24 | BenchmarksPage.propTypes = { 25 | children: PropTypes.node, 26 | }; 27 | 28 | export default BenchmarksPage; 29 | -------------------------------------------------------------------------------- /app/containers/BenchmarksPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarksPage Messages 3 | * 4 | * This contains all the text for the BenchmarksPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.BenchmarksPage.header', 11 | defaultMessage: 'Benchmarks', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/BenchmarksPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | /** 3 | * Direct selector to the BenchmarksPage state domain 4 | */ 5 | const selectBenchmarksPageDomain = () => (state) => state.get('benchmarks'); 6 | 7 | const makeSelectConfigByName = (nameWrapper) => createSelector( 8 | selectBenchmarksPageDomain(), 9 | (substate) => { 10 | const name = nameWrapper[0]; 11 | const config = substate.get('configs').find((c) => c.getIn(['metadata', 'name']) === name); 12 | if (config === undefined) { 13 | return undefined; 14 | } 15 | return config.toJS(); 16 | } 17 | ); 18 | 19 | const makeSelectInstanceByName = (nameWrapper) => createSelector( 20 | selectBenchmarksPageDomain(), 21 | (substate) => { 22 | const name = nameWrapper[0]; 23 | const ins = substate.get('instances').find((instances) => instances.getIn(['metadata', 'name']) === name); 24 | if (ins === undefined) { 25 | return undefined; 26 | } 27 | return ins.toJS(); 28 | } 29 | ); 30 | 31 | const makeSelectLoading = () => createSelector( 32 | selectBenchmarksPageDomain(), 33 | (substate) => substate.get('loading') 34 | ); 35 | 36 | const makeSelectError = () => createSelector( 37 | selectBenchmarksPageDomain(), 38 | (substate) => substate.get('error') 39 | ); 40 | 41 | const makeSelectConfigs = () => createSelector( 42 | selectBenchmarksPageDomain(), 43 | (substate) => substate.get('configs').toJS(), 44 | ); 45 | 46 | const makeSelectInstances = () => createSelector( 47 | selectBenchmarksPageDomain(), 48 | (substate) => substate.get('instances').toJS(), 49 | ); 50 | 51 | const makeSelectInstancesByLabels = (labelsWrapper) => createSelector( 52 | selectBenchmarksPageDomain(), 53 | (substate) => { 54 | const labels = labelsWrapper[0]; 55 | return substate.get('instances').filter((instance) => { 56 | const insLabels = instance.getIn(['metadata', 'labels']); 57 | return Object.keys(labels).map((k) => insLabels.get(k) === labels[k]).reduce((a, b) => a && b); 58 | }).toJS(); 59 | } 60 | ); 61 | 62 | export { 63 | makeSelectConfigs, 64 | makeSelectError, 65 | makeSelectLoading, 66 | makeSelectConfigByName, 67 | makeSelectInstances, 68 | makeSelectInstanceByName, 69 | makeSelectInstancesByLabels, 70 | }; 71 | -------------------------------------------------------------------------------- /app/containers/EnvironmentCreatePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentCreatePage actions 4 | * 5 | */ 6 | 7 | import { 8 | CREATE_ENVIRONMENT_REQUEST, 9 | } from 'containers/EnvironmentsPage/constants'; 10 | 11 | export function createEnvironmentAction(environment) { 12 | return { 13 | type: CREATE_ENVIRONMENT_REQUEST, 14 | environment, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/EnvironmentCreatePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentCreatePage Messages 3 | * 4 | * This contains all the text for the EnvironmentCreatePage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.EnvironmentCreatePage.header', 11 | defaultMessage: 'This is EnvironmentCreatePage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/EnvironmentCreatePage/sagas.js: -------------------------------------------------------------------------------- 1 | 2 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | import { createEnvironment } from 'utils/api'; 5 | import { CREATE_ENVIRONMENT_REQUEST, CREATE_ENVIRONMENT_SUCCESS, CREATE_ENVIRONMENT_ERROR } from 'containers/EnvironmentsPage/constants'; 6 | import { browserHistory } from 'react-router'; 7 | 8 | // Individual exports for testing 9 | function* createEnvironmentSagaRequest(action) { 10 | try { 11 | const data = yield call(createEnvironment, action.environment); 12 | yield put({ type: CREATE_ENVIRONMENT_SUCCESS, data }); 13 | browserHistory.push('/environments'); 14 | } catch (error) { 15 | yield put({ type: CREATE_ENVIRONMENT_ERROR, error }); 16 | } 17 | } 18 | 19 | // Individual exports for testing 20 | export function* defaultSaga() { 21 | // See example in containers/HomePage/sagas.js 22 | const watcher = yield takeLatest(CREATE_ENVIRONMENT_REQUEST, createEnvironmentSagaRequest); 23 | 24 | // Suspend execution until location changes 25 | yield take(LOCATION_CHANGE); 26 | yield cancel(watcher); 27 | } 28 | 29 | // All sagas to be loaded 30 | export default [ 31 | defaultSaga, 32 | ]; 33 | -------------------------------------------------------------------------------- /app/containers/EnvironmentEditPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentEditPage actions 4 | * 5 | */ 6 | 7 | import { 8 | GET_ENVIRONMENT_REQUEST, 9 | EDIT_ENVIRONMENT_REQUEST, 10 | } from 'containers/EnvironmentsPage/constants'; 11 | 12 | export function getEnvironmentAction(name) { 13 | return { 14 | type: GET_ENVIRONMENT_REQUEST, 15 | name, 16 | }; 17 | } 18 | 19 | export function editEnvironmentAction(environment) { 20 | return { 21 | type: EDIT_ENVIRONMENT_REQUEST, 22 | environment, 23 | }; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/containers/EnvironmentEditPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentEditPage Messages 3 | * 4 | * This contains all the text for the EnvironmentEditPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.EnvironmentEditPage.header', 11 | defaultMessage: 'This is EnvironmentEditPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/EnvironmentEditPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { LOCATION_CHANGE } from 'react-router-redux'; 3 | import { getEnvironment, updateEnvironment } from 'utils/api'; 4 | import { GET_ENVIRONMENT_REQUEST, GET_ENVIRONMENT_SUCCESS, GET_ENVIRONMENT_ERROR, EDIT_ENVIRONMENT_REQUEST, EDIT_ENVIRONMENT_SUCCESS, EDIT_ENVIRONMENT_ERROR } from 'containers/EnvironmentsPage/constants'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | // Individual exports for testing 8 | function* getEnvironmentSagaRequest(action) { 9 | try { 10 | const data = yield call(getEnvironment, action.name); 11 | yield put({ type: GET_ENVIRONMENT_SUCCESS, data }); 12 | } catch (error) { 13 | yield put({ type: GET_ENVIRONMENT_ERROR, error }); 14 | } 15 | } 16 | 17 | function* editEnvironmentSagaRequest(action) { 18 | try { 19 | const data = yield call(updateEnvironment, action.environment); 20 | yield put({ type: EDIT_ENVIRONMENT_SUCCESS, data }); 21 | browserHistory.push('/environments'); 22 | } catch (error) { 23 | yield put({ type: EDIT_ENVIRONMENT_ERROR, error }); 24 | } 25 | } 26 | 27 | // Individual exports for testing 28 | export function* getEnvironmentSaga() { 29 | // See example in containers/HomePage/sagas.js 30 | const watcher = yield takeLatest(GET_ENVIRONMENT_REQUEST, getEnvironmentSagaRequest); 31 | 32 | // Suspend execution until location changes 33 | yield take(LOCATION_CHANGE); 34 | yield cancel(watcher); 35 | } 36 | 37 | export function* editEnvironmentSaga() { 38 | // See example in containers/HomePage/sagas.js 39 | const watcher = yield takeLatest(EDIT_ENVIRONMENT_REQUEST, editEnvironmentSagaRequest); 40 | 41 | // Suspend execution until location changes 42 | yield take(LOCATION_CHANGE); 43 | yield cancel(watcher); 44 | } 45 | 46 | // All sagas to be loaded 47 | export default [ 48 | getEnvironmentSaga, 49 | editEnvironmentSaga, 50 | ]; 51 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsListPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentsListPage actions 4 | * 5 | */ 6 | 7 | import { 8 | LOAD_ENVIRONMENTS_REQUEST, 9 | DELETE_ENVIRONMENT_REQUEST, 10 | } from 'containers/EnvironmentsPage/constants'; 11 | 12 | export function removeEnvironmentAction(environment) { 13 | return { 14 | type: DELETE_ENVIRONMENT_REQUEST, 15 | environment, 16 | }; 17 | } 18 | export function loadEnvironmentAction() { 19 | return { 20 | type: LOAD_ENVIRONMENTS_REQUEST, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsListPage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentsListPage 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import Helmet from 'react-helmet'; 11 | import { Link } from 'react-router'; 12 | import { createStructuredSelector } from 'reselect'; 13 | import EnvironmentsList from 'components/EnvironmentsList'; 14 | import { makeSelectEnvironments, makeSelectError, makeSelectLoading } from 'containers/EnvironmentsPage/selectors'; 15 | import commonMessages from 'messages'; 16 | import { loadEnvironmentAction, removeEnvironmentAction } from './actions'; 17 | 18 | export class EnvironmentsListPage extends React.Component { // eslint-disable-line react/prefer-stateless-function 19 | constructor() { 20 | super(); 21 | this.onRemove = this.onRemove.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | this.props.loadEnvironmentData(); 26 | } 27 | 28 | onRemove(environment) { 29 | this.props.removeEnvironment(environment); 30 | } 31 | 32 | render() { 33 | const { loading, error, environments } = this.props; 34 | const environmentsListProps = { 35 | loading, 36 | error, 37 | environments, 38 | }; 39 | return ( 40 |
    41 | 44 | 45 | 46 |
    47 | ); 48 | } 49 | } 50 | 51 | EnvironmentsListPage.propTypes = { 52 | loading: PropTypes.bool, 53 | error: PropTypes.oneOfType([ 54 | PropTypes.object, 55 | PropTypes.bool, 56 | ]), 57 | environments: PropTypes.oneOfType([ 58 | PropTypes.object, 59 | PropTypes.array, 60 | ]), 61 | loadEnvironmentData: PropTypes.func, 62 | removeEnvironment: PropTypes.func, 63 | }; 64 | 65 | const mapStateToProps = createStructuredSelector({ 66 | environments: makeSelectEnvironments(), 67 | loading: makeSelectLoading(), 68 | error: makeSelectError(), 69 | }); 70 | 71 | function mapDispatchToProps(dispatch) { 72 | return { 73 | loadEnvironmentData: () => dispatch(loadEnvironmentAction()), 74 | removeEnvironment: (environment) => dispatch(removeEnvironmentAction(environment)), 75 | }; 76 | } 77 | 78 | export default connect(mapStateToProps, mapDispatchToProps)(EnvironmentsListPage); 79 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsListPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentsListPage Messages 3 | * 4 | * This contains all the text for the EnvironmentsListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.EnvironmentsListPage.header', 11 | defaultMessage: 'This is EnvironmentsListPage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsListPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, take, cancel } from 'redux-saga/effects'; 2 | import { getEnvironments, removeEnvironment } from 'utils/api'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | import { LOAD_ENVIRONMENTS_REQUEST, LOAD_ENVIRONMENTS_SUCCESS, LOAD_ENVIRONMENTS_ERROR, DELETE_ENVIRONMENT_REQUEST, DELETE_ENVIRONMENT_SUCCESS, DELETE_ENVIRONMENT_ERROR } from 'containers/EnvironmentsPage/constants'; 5 | 6 | function* loadEnvironments() { 7 | try { 8 | const data = yield call(getEnvironments); 9 | yield put({ type: LOAD_ENVIRONMENTS_SUCCESS, data }); 10 | } catch (error) { 11 | yield put({ type: LOAD_ENVIRONMENTS_ERROR, error }); 12 | } 13 | } 14 | 15 | function* removeEnvironmentSaga(action) { 16 | try { 17 | yield call(removeEnvironment, action.environment); 18 | yield put({ type: DELETE_ENVIRONMENT_SUCCESS, environment: action.environment }); 19 | } catch (error) { 20 | yield put({ type: DELETE_ENVIRONMENT_ERROR, error }); 21 | } 22 | } 23 | 24 | // Individual exports for testing 25 | export function* getAllSaga() { 26 | const watcher = yield takeLatest(LOAD_ENVIRONMENTS_REQUEST, loadEnvironments); 27 | 28 | // Suspend execution until location changes 29 | yield take(LOCATION_CHANGE); 30 | yield cancel(watcher); 31 | } 32 | export function* removeSaga() { 33 | const watcher = yield takeLatest(DELETE_ENVIRONMENT_REQUEST, removeEnvironmentSaga); 34 | 35 | // Suspend execution until location changes 36 | yield take(LOCATION_CHANGE); 37 | yield cancel(watcher); 38 | } 39 | 40 | // All sagas to be loaded 41 | export default [ 42 | getAllSaga, 43 | removeSaga, 44 | ]; 45 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentsPage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/EnvironmentsListPage/DEFAULT_ACTION'; 8 | export const LOAD_ENVIRONMENTS_REQUEST = 'app/Environments/LOAD_ENVIRONMENTS_REQUEST'; 9 | export const LOAD_ENVIRONMENTS_SUCCESS = 'app/Environments/LOAD_ENVIRONMENTS_SUCCESS'; 10 | export const LOAD_ENVIRONMENTS_ERROR = 'app/Environments/LOAD_ENVIRONMENTS_ERROR'; 11 | export const CREATE_ENVIRONMENT_REQUEST = 'app/Environments/CREATE_ENVIRONMENT_REQUEST'; 12 | export const CREATE_ENVIRONMENT_SUCCESS = 'app/Environments/CREATE_ENVIRONMENT_SUCCESS'; 13 | export const CREATE_ENVIRONMENT_ERROR = 'app/Environments/CREATE_ENVIRONMENT_ERROR'; 14 | export const DELETE_ENVIRONMENT_REQUEST = 'app/Environments/DELETE_ENVIRONMENT_REQUEST'; 15 | export const DELETE_ENVIRONMENT_SUCCESS = 'app/Environments/DELETE_ENVIRONMENT_SUCCESS'; 16 | export const DELETE_ENVIRONMENT_ERROR = 'app/Environments/DELETE_ENVIRONMENT_ERROR'; 17 | export const GET_ENVIRONMENT_REQUEST = 'app/Environments/GET_ENVIRONMENT_REQUEST'; 18 | export const GET_ENVIRONMENT_SUCCESS = 'app/Environments/GET_ENVIRONMENT_SUCCESS'; 19 | export const GET_ENVIRONMENT_ERROR = 'app/Environments/GET_ENVIRONMENT_ERROR'; 20 | export const EDIT_ENVIRONMENT_REQUEST = 'app/Environments/EDIT_ENVIRONMENT_REQUEST'; 21 | export const EDIT_ENVIRONMENT_SUCCESS = 'app/Environments/EDIT_ENVIRONMENT_SUCCESS'; 22 | export const EDIT_ENVIRONMENT_ERROR = 'app/Environments/EDIT_ENVIRONMENT_ERROR'; 23 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentsPage 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import messages from './messages'; 10 | 11 | export class EnvironmentsPage extends React.Component { // eslint-disable-line react/prefer-stateless-function 12 | 13 | render() { 14 | return ( 15 |
    16 |

    17 | 18 |

    19 | {React.Children.toArray(this.props.children)} 20 |
    21 | ); 22 | } 23 | } 24 | 25 | EnvironmentsPage.propTypes = { 26 | children: PropTypes.node, 27 | }; 28 | 29 | 30 | export default EnvironmentsPage; 31 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentsPage Messages 3 | * 4 | * This contains all the text for the EnvironmentsPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.EnvironmentsPage.header', 11 | defaultMessage: 'Environments', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EnvironmentsPage reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | LOAD_ENVIRONMENTS_REQUEST, 10 | LOAD_ENVIRONMENTS_SUCCESS, 11 | LOAD_ENVIRONMENTS_ERROR, 12 | CREATE_ENVIRONMENT_REQUEST, 13 | CREATE_ENVIRONMENT_SUCCESS, 14 | CREATE_ENVIRONMENT_ERROR, 15 | DELETE_ENVIRONMENT_SUCCESS, 16 | GET_ENVIRONMENT_SUCCESS, 17 | GET_ENVIRONMENT_REQUEST, 18 | GET_ENVIRONMENT_ERROR, 19 | EDIT_ENVIRONMENT_SUCCESS, 20 | } from './constants'; 21 | 22 | const initialState = fromJS({ environments: [], loading: false, error: false }); 23 | 24 | function environmentsReducer(state = initialState, action) { 25 | switch (action.type) { 26 | case EDIT_ENVIRONMENT_SUCCESS: 27 | return state.set('environments', state.get('environments').map((env) => 28 | env.getIn(['metadata', 'name']) === action.data.metadata.name ? fromJS(action.data) : env 29 | )); 30 | case GET_ENVIRONMENT_REQUEST: 31 | return state 32 | .set('loading', true) 33 | .set('error', false); 34 | case GET_ENVIRONMENT_ERROR: 35 | return state 36 | .set('error', fromJS(action.error)) 37 | .set('loading', false); 38 | case GET_ENVIRONMENT_SUCCESS: 39 | return state 40 | .update('environments', (env) => env.push(fromJS(action.data))) 41 | .set('loading', false); 42 | case CREATE_ENVIRONMENT_REQUEST: 43 | return state 44 | .set('loading', true) 45 | .set('error', false); 46 | case CREATE_ENVIRONMENT_SUCCESS: 47 | return state 48 | .update('environments', (env) => env.push(fromJS(action.data))) 49 | .set('loading', false) 50 | .set('error', false); 51 | case CREATE_ENVIRONMENT_ERROR: 52 | return state 53 | .set('loading', false) 54 | .set('error', fromJS(action.error)); 55 | case DELETE_ENVIRONMENT_SUCCESS: 56 | return state.set('environments', state.get('environments').filter((e) => 57 | e.getIn(['metadata', 'name']) !== action.environment.name 58 | )); 59 | case LOAD_ENVIRONMENTS_REQUEST: 60 | return state 61 | .set('loading', true) 62 | .set('error', false) 63 | .set('environments', fromJS([])); 64 | case LOAD_ENVIRONMENTS_ERROR: 65 | return state 66 | .set('error', fromJS(action.error)) 67 | .set('loading', false); 68 | case LOAD_ENVIRONMENTS_SUCCESS: 69 | return state 70 | .set('environments', fromJS(action.data)) 71 | .set('loading', false); 72 | default: 73 | return state; 74 | } 75 | } 76 | 77 | export default environmentsReducer; 78 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the environmentEditPage state domain 5 | */ 6 | const selectEnvironmentsPageDomain = () => (state) => state.get('environments'); 7 | 8 | 9 | const makeSelectEnvironmentByName = () => createSelector( 10 | selectEnvironmentsPageDomain(), 11 | (substate) => (environmentName) => { 12 | const environmentFound = substate.get('environments').find((environment) => environment.getIn(['metadata', 'name']) === environmentName); 13 | if (environmentFound) { 14 | return ({ name: environmentFound.getIn(['metadata', 'name']), image: environmentFound.get('runContainerImageUrl') }); 15 | } 16 | return false; 17 | } 18 | ); 19 | 20 | const makeSelectLoading = () => createSelector( 21 | selectEnvironmentsPageDomain(), 22 | (substate) => substate.get('loading') 23 | ); 24 | 25 | const makeSelectError = () => createSelector( 26 | selectEnvironmentsPageDomain(), 27 | (substate) => substate.get('error') 28 | ); 29 | 30 | const makeSelectEnvironments = () => createSelector( 31 | selectEnvironmentsPageDomain(), 32 | (substate) => substate.get('environments').map((e) => ({ name: e.getIn(['metadata', 'name']), image: e.get('runContainerImageUrl') })).toJS() 33 | ); 34 | 35 | 36 | export { 37 | makeSelectEnvironmentByName, 38 | makeSelectEnvironments, 39 | makeSelectError, 40 | makeSelectLoading, 41 | }; 42 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fromJS } from 'immutable'; 3 | import environmentsPageReducer from '../reducer'; 4 | 5 | describe('environmentsPageReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(environmentsPageReducer(undefined, {})).toEqual(fromJS({ environments: [], loading: false, error: false })); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/EnvironmentsPage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { 3 | makeSelectEnvironmentByName, 4 | makeSelectEnvironments, 5 | makeSelectError, 6 | makeSelectLoading, 7 | } from '../selectors'; 8 | 9 | const mockedState = fromJS({ 10 | environments: { 11 | environments: [ 12 | { 13 | metadata: { 14 | name: 'node', 15 | uid: 'a9b76a1f-ee1f-4177-83ee-2c9a016a5da3', 16 | }, 17 | runContainerImageUrl: 'fission/node-env', 18 | }, 19 | { 20 | metadata: { 21 | name: 'python', 22 | uid: '830e152b-771d-4cc5-8e3e-ea323b698aa7', 23 | }, 24 | runContainerImageUrl: 'fission/python-env', 25 | }, 26 | ], 27 | loading: false, 28 | error: false, 29 | }, 30 | }); 31 | 32 | describe('makeSelectEnvironmentByName', () => { 33 | it('should select the existed python environment', () => { 34 | expect(makeSelectEnvironmentByName()(mockedState)('python')) 35 | .toEqual({ image: 'fission/python-env', name: 'python' }); 36 | }); 37 | it('should return false as the non-existed php environment', () => { 38 | expect(makeSelectEnvironmentByName()(mockedState)('php')) 39 | .toEqual(false); 40 | }); 41 | }); 42 | 43 | describe('makeSelectEnvironments', () => { 44 | it('should select the environment list', () => { 45 | expect(makeSelectEnvironments()(mockedState)) 46 | .toEqual([ 47 | { 48 | name: 'node', 49 | image: 'fission/node-env', 50 | }, 51 | { 52 | name: 'python', 53 | image: 'fission/python-env', 54 | }, 55 | ]); 56 | }); 57 | it('should select the empty environment list', () => { 58 | expect(makeSelectEnvironments()(mockedState.setIn(['environments', 'environments'], fromJS([])))) 59 | .toEqual([]); 60 | }); 61 | }); 62 | 63 | describe('makeSelectLoading', () => { 64 | it('should select the loading', () => { 65 | expect(makeSelectLoading()(mockedState)) 66 | .toEqual(false); 67 | }); 68 | }); 69 | 70 | describe('makeSelectError', () => { 71 | it('should select the error', () => { 72 | expect(makeSelectError()(mockedState)) 73 | .toEqual(false); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /app/containers/FunctionCreatePage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_FUNCTION_REQUEST, 3 | TEST_FUNCTION_REQUEST, 4 | CLEAN_TEST_FUNCTION_REQUEST, 5 | } from 'containers/FunctionsPage/constants'; 6 | 7 | export function createFunctionAction(fn) { 8 | return { 9 | type: CREATE_FUNCTION_REQUEST, 10 | fn, 11 | }; 12 | } 13 | 14 | export function testFunctionAction(fn) { 15 | return { 16 | type: TEST_FUNCTION_REQUEST, 17 | fn, 18 | }; 19 | } 20 | 21 | export function cleanTestFunctionAction() { 22 | return { 23 | type: CLEAN_TEST_FUNCTION_REQUEST, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/FunctionCreatePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * FunctionCreatePage Messages 3 | * 4 | * This contains all the text for the FunctionCreatePage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.FunctionCreatePage.header', 11 | defaultMessage: 'This is FunctionCreatePage container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/FunctionCreatePage/sagas.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, cancel, takeLatest } from 'redux-saga/effects'; 2 | import { delay } from 'redux-saga'; 3 | import v4 from 'uuid'; 4 | import { LOCATION_CHANGE } from 'react-router-redux'; 5 | import { browserHistory } from 'react-router'; 6 | import { postFunction, restRequest, removeFunction } from 'utils/api'; 7 | import { 8 | CREATE_FUNCTION_REQUEST, 9 | CREATE_FUNCTION_SUCCESS, 10 | CREATE_FUNCTION_ERROR, 11 | TEST_FUNCTION_REQUEST, 12 | TEST_FUNCTION_SUCCESS, 13 | TEST_FUNCTION_ERROR, 14 | } from 'containers/FunctionsPage/constants'; 15 | 16 | function* createFunction(action) { 17 | try { 18 | yield call(postFunction, action.fn); 19 | 20 | yield put({ type: CREATE_FUNCTION_SUCCESS, data: action.fn }); 21 | 22 | // TODO the following code works, but not sure it is the best solution 23 | browserHistory.push(`/functions/edit/${action.fn.name}`); 24 | } catch (error) { 25 | yield put({ type: CREATE_FUNCTION_ERROR, error }); 26 | } 27 | } 28 | function* testFunction(action) { 29 | const { fn } = action; 30 | const { method, headers, params, body, draft } = fn.test; 31 | if (draft) { 32 | fn.name = v4(); 33 | } 34 | const url = `/fission-function/${fn.name}`; 35 | 36 | try { 37 | if (draft) { 38 | yield call(postFunction, fn); 39 | yield delay(4 * 1000); 40 | } 41 | const data = yield call(restRequest, url, method, headers, params, body); 42 | if (draft) { 43 | yield call(removeFunction, fn); 44 | } 45 | 46 | yield put({ type: TEST_FUNCTION_SUCCESS, data }); 47 | } catch (error) { 48 | yield put({ type: TEST_FUNCTION_ERROR, error }); 49 | } 50 | } 51 | 52 | export function* createFunctionSaga() { 53 | const watcher = yield takeLatest(CREATE_FUNCTION_REQUEST, createFunction); 54 | 55 | // Suspend execution until location changes 56 | yield take(LOCATION_CHANGE); 57 | yield cancel(watcher); 58 | } 59 | export function* testFunctionSaga() { 60 | const watcher = yield takeLatest(TEST_FUNCTION_REQUEST, testFunction); 61 | 62 | // Suspend execution until location changes 63 | yield take(LOCATION_CHANGE); 64 | yield cancel(watcher); 65 | } 66 | 67 | // All sagas to be loaded 68 | export default [ 69 | createFunctionSaga, 70 | testFunctionSaga, 71 | ]; 72 | -------------------------------------------------------------------------------- /app/containers/FunctionEditPage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_FUNCTION_REQUEST, 3 | LOAD_TRIGGERSHTTP_REQUEST, 4 | DELETE_TRIGGERHTTP_REQUEST, 5 | UPDATE_FUNCTION_REQUEST, 6 | CREATE_TRIGGERHTTP_REQUEST, 7 | CREATE_FUNCTION_REQUEST, 8 | CREATE_KUBEWATCHER_REQUEST, 9 | DELETE_KUBEWATCHER_REQUEST, 10 | LOAD_KUBEWATCHERS_REQUEST, 11 | LOAD_TRIGGERSTIMER_REQUEST, 12 | DELETE_TRIGGERTIMER_REQUEST, 13 | CREATE_TRIGGERTIMER_REQUEST, 14 | LOAD_TRIGGERSMQ_REQUEST, 15 | DELETE_TRIGGERMQ_REQUEST, 16 | CREATE_TRIGGERMQ_REQUEST, 17 | } from 'containers/FunctionsPage/constants'; 18 | 19 | 20 | export function getFunctionAction(name) { 21 | return { 22 | type: GET_FUNCTION_REQUEST, 23 | name, 24 | }; 25 | } 26 | 27 | export function loadTriggersHttpAction() { 28 | return { 29 | type: LOAD_TRIGGERSHTTP_REQUEST, 30 | }; 31 | } 32 | 33 | export function deleteTriggerHttpAction(trigger) { 34 | return { 35 | type: DELETE_TRIGGERHTTP_REQUEST, 36 | trigger, 37 | }; 38 | } 39 | 40 | export function updateFunctionAction(fn) { 41 | return { 42 | type: UPDATE_FUNCTION_REQUEST, 43 | fn, 44 | }; 45 | } 46 | 47 | export function createTriggerHttpAction(trigger) { 48 | return { 49 | type: CREATE_TRIGGERHTTP_REQUEST, 50 | trigger, 51 | }; 52 | } 53 | 54 | export function createFunctionAction(fn) { 55 | return { 56 | type: CREATE_FUNCTION_REQUEST, 57 | fn, 58 | }; 59 | } 60 | 61 | export function createKubeWatcherAction(watcher) { 62 | return { 63 | type: CREATE_KUBEWATCHER_REQUEST, 64 | watcher, 65 | }; 66 | } 67 | 68 | export function deleteKubeWatcherAction(watcher) { 69 | return { 70 | type: DELETE_KUBEWATCHER_REQUEST, 71 | watcher, 72 | }; 73 | } 74 | 75 | export function loadKubeWatchersAction() { 76 | return { 77 | type: LOAD_KUBEWATCHERS_REQUEST, 78 | }; 79 | } 80 | 81 | export function createTriggerTimerAction(trigger) { 82 | return { 83 | type: CREATE_TRIGGERTIMER_REQUEST, 84 | trigger, 85 | }; 86 | } 87 | 88 | export function deleteTriggerTimerAction(trigger) { 89 | return { 90 | type: DELETE_TRIGGERTIMER_REQUEST, 91 | trigger, 92 | }; 93 | } 94 | 95 | export function loadTriggersTimerAction() { 96 | return { 97 | type: LOAD_TRIGGERSTIMER_REQUEST, 98 | }; 99 | } 100 | 101 | 102 | export function createTriggerMQAction(trigger) { 103 | return { 104 | type: CREATE_TRIGGERMQ_REQUEST, 105 | trigger, 106 | }; 107 | } 108 | 109 | export function deleteTriggerMQAction(trigger) { 110 | return { 111 | type: DELETE_TRIGGERMQ_REQUEST, 112 | trigger, 113 | }; 114 | } 115 | 116 | export function loadTriggersMQAction() { 117 | return { 118 | type: LOAD_TRIGGERSMQ_REQUEST, 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /app/containers/FunctionEditPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LocaleToggle Messages 3 | * 4 | * This contains all the text for the LanguageToggle component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/FunctionListItem/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * EnvironmentsListItem Messages 3 | * 4 | * This contains all the text for the EnvironmentsListItem component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | httptriggers: { 10 | id: 'app.containers.FunctionListItem.httptriggers', 11 | defaultMessage: 'Http Triggers', 12 | }, 13 | kubewatchers: { 14 | id: 'app.containers.FunctionListItem.kubewatchers', 15 | defaultMessage: 'Kube Watchers', 16 | }, 17 | timertriggers: { 18 | id: 'app.containers.FunctionListItem.timertriggers', 19 | defaultMessage: 'Timer Triggers', 20 | }, 21 | mqtriggers: { 22 | id: 'app.containers.FunctionListItem.mqtriggers', 23 | defaultMessage: 'MQ Triggers', 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /app/containers/FunctionUploadPage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPLOAD_FUNCTIONS_IN_BATCH_REQUEST, 3 | SET_UPLOAD_FUNCTIONS, 4 | } from 'containers/FunctionsPage/constants'; 5 | 6 | export function uploadFunctionsInBatchAction(fns, isCreate) { 7 | return { 8 | type: UPLOAD_FUNCTIONS_IN_BATCH_REQUEST, 9 | fns, 10 | isCreate, 11 | }; 12 | } 13 | 14 | export function setUploadFunctionsAction(fns) { 15 | return { 16 | type: SET_UPLOAD_FUNCTIONS, 17 | data: fns, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/containers/FunctionUploadPage/listItem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * FunctionUploadListItem 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import { Link } from 'react-router'; 10 | import commonMessages from 'messages'; 11 | 12 | export class ListItem extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | const { item, onRemove } = this.props; 15 | return ( 16 | 17 | { item.name } 18 | { item.environment } 19 | 20 | 21 | { item.errors.length > 0 && 22 | (
      23 | { 24 | item.errors.map((e, idx) =>
    • {e}
    • ) 25 | } 26 |
    ) 27 | } 28 | 29 | 30 | { 31 | item.status === 'uploaded' && 32 | 33 | } 34 | 35 | 36 | 37 | ); 38 | } 39 | } 40 | 41 | ListItem.propTypes = { 42 | item: PropTypes.object, 43 | onRemove: PropTypes.func, 44 | }; 45 | 46 | export default ListItem; 47 | -------------------------------------------------------------------------------- /app/containers/FunctionUploadPage/sagas.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, cancel, takeLatest } from 'redux-saga/effects'; 2 | import { putFunction, postFunction } from 'utils/api'; 3 | import { 4 | UPLOAD_FUNCTIONS_IN_BATCH_REQUEST, 5 | UPLOAD_SINGLE_FUNCTION_IN_BATCH_PROGRESS, 6 | UPLOAD_SINGLE_FUNCTION_IN_BATCH_ERROR, 7 | } from 'containers/FunctionsPage/constants'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | 10 | function* uploadFunctions(action) { 11 | const apiFunction = action.isCreate ? postFunction : putFunction; 12 | for (let i = 0; i < action.fns.length; i += 1) { 13 | const fn = action.fns[i]; 14 | try { 15 | fn.status = 'processing'; 16 | yield put({ type: UPLOAD_SINGLE_FUNCTION_IN_BATCH_PROGRESS, data: Object.assign({}, fn) }); 17 | yield call(apiFunction, fn); 18 | fn.status = 'uploaded'; 19 | yield put({ type: UPLOAD_SINGLE_FUNCTION_IN_BATCH_PROGRESS, data: Object.assign({}, fn) }); 20 | } catch (error) { 21 | fn.errors = error.response ? [error.response.data] : [JSON.stringify(error)]; 22 | fn.status = 'failed'; 23 | yield put({ type: UPLOAD_SINGLE_FUNCTION_IN_BATCH_ERROR, data: Object.assign({}, fn) }); 24 | } 25 | } 26 | } 27 | 28 | export function* uploadFunctionsSaga() { 29 | const watcher = yield takeLatest(UPLOAD_FUNCTIONS_IN_BATCH_REQUEST, uploadFunctions); 30 | 31 | // Suspend execution until location changes 32 | yield take(LOCATION_CHANGE); 33 | yield cancel(watcher); 34 | } 35 | 36 | // All sagas to be loaded 37 | export default [ 38 | uploadFunctionsSaga, 39 | ]; 40 | -------------------------------------------------------------------------------- /app/containers/FunctionsListPage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOAD_FUNCTIONS_REQUEST, 3 | LOAD_TRIGGERSHTTP_REQUEST, 4 | LOAD_TRIGGERSTIMER_REQUEST, 5 | LOAD_TRIGGERSMQ_REQUEST, 6 | DELETE_FUNCTION_REQUEST, 7 | LOAD_KUBEWATCHERS_REQUEST, 8 | } from 'containers/FunctionsPage/constants'; 9 | 10 | 11 | export function loadFunctionAction() { 12 | return { 13 | type: LOAD_FUNCTIONS_REQUEST, 14 | }; 15 | } 16 | 17 | export function loadTriggersHttpAction() { 18 | return { 19 | type: LOAD_TRIGGERSHTTP_REQUEST, 20 | }; 21 | } 22 | 23 | export function loadTriggersTimerAction() { 24 | return { 25 | type: LOAD_TRIGGERSTIMER_REQUEST, 26 | }; 27 | } 28 | 29 | export function loadTriggersMQAction() { 30 | return { 31 | type: LOAD_TRIGGERSMQ_REQUEST, 32 | }; 33 | } 34 | 35 | export function loadKubeWatchersAction() { 36 | return { 37 | type: LOAD_KUBEWATCHERS_REQUEST, 38 | }; 39 | } 40 | 41 | export function deleteFunctionAction(fn) { 42 | return { 43 | type: DELETE_FUNCTION_REQUEST, 44 | fn, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /app/containers/FunctionsListPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * FunctionsListPage Messages 3 | * 4 | * This contains all the text for the FunctionsListPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.FunctionsListPage.header', 11 | defaultMessage: 'This is FunctionsListPage container !', 12 | }, 13 | functionDeleteRelatedTriggers: { 14 | id: 'app.containers.FunctionsPage.function.delete.triggers', 15 | defaultMessage: 'The function has related triggers, delete these triggers?', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /app/containers/FunctionsPage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * FunctionsPage 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import messages from './messages'; 10 | 11 | export class FunctionsPage extends React.Component { // eslint-disable-line react/prefer-stateless-function 12 | render() { 13 | return ( 14 |
    15 |

    16 | 17 |

    18 | {React.Children.toArray(this.props.children)} 19 |
    20 | ); 21 | } 22 | } 23 | 24 | FunctionsPage.propTypes = { 25 | children: PropTypes.node, 26 | }; 27 | 28 | export default FunctionsPage; 29 | -------------------------------------------------------------------------------- /app/containers/FunctionsPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * FunctionsPage Messages 3 | * 4 | * This contains all the text for the FunctionsPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.FunctionsPage.header', 11 | defaultMessage: 'Functions', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/FunctionsPage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('functionsPageReducer', () => { 3 | it('returns the initial state', () => { 4 | expect(true).toEqual(true); 5 | }); 6 | 7 | // TODO add test for some actions of function page reducer 8 | }); 9 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | export const DEFAULT_LOCALE = 'en'; 9 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | 14 | import { makeSelectLocale } from './selectors'; 15 | 16 | export class LanguageProvider extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 17 | render() { 18 | return ( 19 | 20 | {React.Children.only(this.props.children)} 21 | 22 | ); 23 | } 24 | } 25 | 26 | LanguageProvider.propTypes = { 27 | locale: React.PropTypes.string, 28 | messages: React.PropTypes.object, 29 | children: React.PropTypes.element.isRequired, 30 | }; 31 | 32 | 33 | const mapStateToProps = createSelector( 34 | makeSelectLocale(), 35 | (locale) => ({ locale }) 36 | ); 37 | 38 | function mapDispatchToProps(dispatch) { 39 | return { 40 | dispatch, 41 | }; 42 | } 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 45 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | 9 | import { 10 | CHANGE_LOCALE, 11 | } from './constants'; 12 | import { 13 | DEFAULT_LOCALE, 14 | } from '../App/constants'; // eslint-disable-line 15 | 16 | const initialState = fromJS({ 17 | locale: DEFAULT_LOCALE, 18 | }); 19 | 20 | function languageProviderReducer(state = initialState, action) { 21 | switch (action.type) { 22 | case CHANGE_LOCALE: 23 | return state 24 | .set('locale', action.locale); 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | export default languageProviderReducer; 31 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = (state) => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | 12 | const makeSelectLocale = () => createSelector( 13 | selectLanguage, 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | makeSelectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | changeLocale, 3 | } from '../actions'; 4 | 5 | import { 6 | CHANGE_LOCALE, 7 | } from '../constants'; 8 | 9 | describe('LanguageProvider actions', () => { 10 | describe('Change Local Action', () => { 11 | it('has a type of CHANGE_LOCALE', () => { 12 | const expected = { 13 | type: CHANGE_LOCALE, 14 | locale: 'de', 15 | }; 16 | expect(changeLocale('de')).toEqual(expected); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import { FormattedMessage, defineMessages } from 'react-intl'; 4 | import { Provider } from 'react-redux'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | import ConnectedLanguageProvider, { LanguageProvider } from '../index'; 8 | import configureStore from '../../../store'; 9 | 10 | import { translationMessages } from '../../../i18n'; 11 | 12 | const messages = defineMessages({ 13 | someMessage: { 14 | id: 'some.id', 15 | defaultMessage: 'This is some default message', 16 | en: 'This is some en message', 17 | }, 18 | }); 19 | 20 | describe('', () => { 21 | it('should render its children', () => { 22 | const children = (

    Test

    ); 23 | const renderedComponent = shallow( 24 | 25 | {children} 26 | 27 | ); 28 | expect(renderedComponent.contains(children)).toBe(true); 29 | }); 30 | }); 31 | 32 | describe('', () => { 33 | let store; 34 | 35 | beforeAll(() => { 36 | store = configureStore({}, browserHistory); 37 | }); 38 | 39 | it('should render the default language messages', () => { 40 | const renderedComponent = mount( 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | expect(renderedComponent.contains()).toBe(true); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import languageProviderReducer from '../reducer'; 4 | import { 5 | CHANGE_LOCALE, 6 | } from '../constants'; 7 | 8 | describe('languageProviderReducer', () => { 9 | it('returns the initial state', () => { 10 | expect(languageProviderReducer(undefined, {})).toEqual(fromJS({ 11 | locale: 'en', 12 | })); 13 | }); 14 | 15 | it('changes the locale', () => { 16 | expect(languageProviderReducer(undefined, { type: CHANGE_LOCALE, locale: 'de' }).toJS()).toEqual({ 17 | locale: 'de', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { 4 | selectLanguage, 5 | } from '../selectors'; 6 | 7 | describe('selectLanguage', () => { 8 | it('should select the global state', () => { 9 | const globalState = fromJS({}); 10 | const mockedState = fromJS({ 11 | language: globalState, 12 | }); 13 | expect(selectLanguage(mockedState)).toEqual(globalState); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | padding: 2px; 5 | `; 6 | 7 | export default Wrapper; 8 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { createSelector } from 'reselect'; 10 | 11 | import Toggle from 'components/Toggle'; 12 | import Wrapper from './Wrapper'; 13 | import messages from './messages'; 14 | import { appLocales } from '../../i18n'; 15 | import { changeLocale } from '../LanguageProvider/actions'; 16 | import { makeSelectLocale } from '../LanguageProvider/selectors'; 17 | 18 | export class LocaleToggle extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 19 | render() { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | LocaleToggle.propTypes = { 29 | onLocaleToggle: React.PropTypes.func, 30 | locale: React.PropTypes.string, 31 | }; 32 | 33 | const mapStateToProps = createSelector( 34 | makeSelectLocale(), 35 | (locale) => ({ locale }) 36 | ); 37 | 38 | export function mapDispatchToProps(dispatch) { 39 | return { 40 | onLocaleToggle: (evt) => dispatch(changeLocale(evt.target.value)), 41 | dispatch, 42 | }; 43 | } 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(LocaleToggle); 46 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LocaleToggle Messages 3 | * 4 | * This contains all the text for the LanguageToggle component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | en: { 10 | id: 'boilerplate.containers.LocaleToggle.en', 11 | defaultMessage: 'en', 12 | }, 13 | zh: { 14 | id: 'boilerplate.containers.LocaleToggle.zh', 15 | defaultMessage: 'zh', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/tests/Wrapper.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Wrapper from '../Wrapper'; 5 | 6 | describe('', () => { 7 | it('should render an
    tag', () => { 8 | const renderedComponent = shallow(); 9 | expect(renderedComponent.type()).toEqual('div'); 10 | }); 11 | 12 | it('should have a className attribute', () => { 13 | const renderedComponent = shallow(); 14 | expect(renderedComponent.prop('className')).toBeDefined(); 15 | }); 16 | 17 | it('should adopt a valid attribute', () => { 18 | const id = 'test'; 19 | const renderedComponent = shallow(); 20 | expect(renderedComponent.prop('id')).toEqual(id); 21 | }); 22 | 23 | it('should not adopt an invalid attribute', () => { 24 | const renderedComponent = shallow(); 25 | expect(renderedComponent.prop('attribute')).toBeUndefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { browserHistory } from 'react-router'; 4 | import { shallow, mount } from 'enzyme'; 5 | 6 | import LocaleToggle, { mapDispatchToProps } from '../index'; 7 | import { changeLocale } from '../../LanguageProvider/actions'; 8 | import LanguageProvider from '../../LanguageProvider'; 9 | 10 | import configureStore from '../../../store'; 11 | import { translationMessages } from '../../../i18n'; 12 | 13 | describe('', () => { 14 | let store; 15 | 16 | beforeAll(() => { 17 | store = configureStore({}, browserHistory); 18 | }); 19 | 20 | it('should render the default language messages', () => { 21 | const renderedComponent = shallow( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | expect(renderedComponent.contains()).toBe(true); 29 | }); 30 | 31 | it('should present the default `en` english language option', () => { 32 | const renderedComponent = mount( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | expect(renderedComponent.contains()).toBe(true); 40 | }); 41 | 42 | describe('mapDispatchToProps', () => { 43 | describe('onLocaleToggle', () => { 44 | it('should be injected', () => { 45 | const dispatch = jest.fn(); 46 | const result = mapDispatchToProps(dispatch); 47 | expect(result.onLocaleToggle).toBeDefined(); 48 | }); 49 | 50 | it('should dispatch changeLocale when called', () => { 51 | const dispatch = jest.fn(); 52 | const result = mapDispatchToProps(dispatch); 53 | const locale = 'de'; 54 | const evt = { target: { value: locale } }; 55 | result.onLocaleToggle(evt); 56 | expect(dispatch).toHaveBeenCalledWith(changeLocale(locale)); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a necessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | 15 | import messages from './messages'; 16 | 17 | export default class NotFound extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 |

    21 | 22 |

    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component!', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import NotFoundPage from '../index'; 6 | import messages from '../messages'; 7 | 8 | describe('', () => { 9 | it('should render the page message', () => { 10 | const renderedComponent = shallow( 11 | 12 | ); 13 | expect(renderedComponent.contains( 14 | 15 | )).toEqual(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/app/favicon.ico -------------------------------------------------------------------------------- /app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | /* eslint no-unused-expressions: 0 */ 4 | injectGlobal` 5 | body { 6 | padding-top: 20px; 7 | padding-bottom: 20px; 8 | } 9 | 10 | .navbar { 11 | margin-bottom: 20px; 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | import enLocaleData from 'react-intl/locale-data/en'; 9 | import zhLocaleData from 'react-intl/locale-data/zh'; 10 | import frLocaleData from 'react-intl/locale-data/fr'; 11 | 12 | import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line 13 | import enTranslationMessages from './translations/en.json'; 14 | import zhTranslationMessages from './translations/zh.json'; 15 | import frTranslationMessages from './translations/fr.json'; 16 | 17 | export const appLocales = [ 18 | 'en', 19 | 'zh', 20 | 'fr', 21 | ]; 22 | 23 | addLocaleData(enLocaleData); 24 | addLocaleData(zhLocaleData); 25 | addLocaleData(frLocaleData); 26 | 27 | export const formatTranslationMessages = (locale, messages) => { 28 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE 29 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) 30 | : {}; 31 | return Object.keys(messages).reduce((formattedMessages, key) => { 32 | let message = messages[key]; 33 | if (!message && locale !== DEFAULT_LOCALE) { 34 | message = defaultFormattedMessages[key]; 35 | } 36 | return Object.assign(formattedMessages, { [key]: message }); 37 | }, {}); 38 | }; 39 | 40 | export const translationMessages = { 41 | en: formatTranslationMessages('en', enTranslationMessages), 42 | zh: formatTranslationMessages('zh', zhTranslationMessages), 43 | fr: formatTranslationMessages('fr', frTranslationMessages), 44 | }; 45 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fission UI 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fission UI", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } 34 | -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | 10 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 11 | 12 | /* 13 | * routeReducer 14 | * 15 | * The reducer merges route location changes into our immutable state. 16 | * The change is necessitated by moving to react-router-redux@4 17 | * 18 | */ 19 | 20 | // Initial routing state 21 | const routeInitialState = fromJS({ 22 | locationBeforeTransitions: null, 23 | }); 24 | 25 | /** 26 | * Merge route into the global application state 27 | */ 28 | function routeReducer(state = routeInitialState, action) { 29 | switch (action.type) { 30 | /* istanbul ignore next */ 31 | case LOCATION_CHANGE: 32 | return state.merge({ 33 | locationBeforeTransitions: action.payload, 34 | }); 35 | default: 36 | return state; 37 | } 38 | } 39 | 40 | /** 41 | * Creates the main reducer with the asynchronously loaded ones 42 | */ 43 | export default function createReducer(asyncReducers) { 44 | return combineReducers({ 45 | route: routeReducer, 46 | language: languageProviderReducer, 47 | ...asyncReducers, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | 13 | export default function configureStore(initialState = {}, history) { 14 | // Create the store with two middlewares 15 | // 1. sagaMiddleware: Makes redux-sagas work 16 | // 2. routerMiddleware: Syncs the location/URL path to the state 17 | const middlewares = [ 18 | sagaMiddleware, 19 | routerMiddleware(history), 20 | ]; 21 | 22 | const enhancers = [ 23 | applyMiddleware(...middlewares), 24 | ]; 25 | 26 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 27 | /* eslint-disable no-underscore-dangle */ 28 | const composeEnhancers = 29 | process.env.NODE_ENV !== 'production' && 30 | typeof window === 'object' && 31 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 32 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 33 | /* eslint-enable */ 34 | 35 | const store = createStore( 36 | createReducer(), 37 | fromJS(initialState), 38 | composeEnhancers(...enhancers) 39 | ); 40 | 41 | // Extensions 42 | store.runSaga = sagaMiddleware.run; 43 | store.asyncReducers = {}; // Async reducer registry 44 | 45 | // Make reducers hot reloadable, see http://mxs.is/googmo 46 | /* istanbul ignore next */ 47 | if (module.hot) { 48 | module.hot.accept('./reducers', () => { 49 | import('./reducers').then((reducerModule) => { 50 | const createReducers = reducerModule.default; 51 | const nextReducers = createReducers(store.asyncReducers); 52 | 53 | store.replaceReducer(nextReducers); 54 | }); 55 | }); 56 | } 57 | 58 | return store; 59 | } 60 | -------------------------------------------------------------------------------- /app/tests/i18n.test.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE } from '../containers/App/constants'; 2 | import { formatTranslationMessages } from '../i18n'; 3 | 4 | jest.mock('../translations/en.json', () => ( 5 | { 6 | message1: 'default message', 7 | message2: 'default message 2', 8 | } 9 | )); 10 | 11 | const esTranslationMessages = { 12 | message1: 'mensaje predeterminado', 13 | message2: '', 14 | }; 15 | 16 | describe('formatTranslationMessages', () => { 17 | it('should build only defaults when DEFAULT_LOCALE', () => { 18 | const result = formatTranslationMessages(DEFAULT_LOCALE, { a: 'a' }); 19 | 20 | expect(result).toEqual({ a: 'a' }); 21 | }); 22 | 23 | 24 | it('should combine default locale and current locale when not DEFAULT_LOCALE', () => { 25 | const result = formatTranslationMessages('', esTranslationMessages); 26 | 27 | expect(result).toEqual({ 28 | message1: 'mensaje predeterminado', 29 | message2: 'default message 2', 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import { browserHistory } from 'react-router'; 6 | import configureStore from '../store'; 7 | 8 | describe('configureStore', () => { 9 | let store; 10 | 11 | beforeAll(() => { 12 | store = configureStore({}, browserHistory); 13 | }); 14 | 15 | describe('asyncReducers', () => { 16 | it('should contain an object for async reducers', () => { 17 | expect(typeof store.asyncReducers).toBe('object'); 18 | }); 19 | }); 20 | 21 | describe('runSaga', () => { 22 | it('should contain a hook for `sagaMiddleware.run`', () => { 23 | expect(typeof store.runSaga).toBe('function'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/translations/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.components.NotFoundPage.header": "抱歉,没有这个页面", 3 | 4 | "boilerplate.containers.LocaleToggle.en": "en", 5 | "boilerplate.containers.LocaleToggle.zh": "中文", 6 | "boilerplate.containers.LocaleToggle.fr": "fr", 7 | 8 | "app.messages.deploy": "部署", 9 | "app.messages.save": "保存", 10 | "app.messages.cancel": "取消", 11 | "app.messages.edit": "编辑", 12 | "app.messages.delete": "删除", 13 | "app.messages.add": "创建", 14 | "app.messages.method": "方法", 15 | "app.messages.path": "路径", 16 | "app.messages.action": "操作", 17 | "app.messages.namespace": "命名空间", 18 | "app.messages.objtype": "对象类型", 19 | "app.messages.labelselector": "标签选择器", 20 | "app.messages.functionName": "函数名称", 21 | "app.messages.environment": "环境", 22 | "app.messages.syntax": "语言", 23 | "app.messages.name": "名称", 24 | "app.messages.trigger": "触发器", 25 | "app.messages.dockerImage": "Docker镜像", 26 | "app.messages.environmentName": "环境名称", 27 | "app.messages.function": "函数", 28 | "app.messages.chooseSample": "从样例选择", 29 | "app.messages.test": "测试", 30 | "app.messages.draft": "草稿模式", 31 | "app.messages.copyEndpoint": "拷贝链接", 32 | "app.messages.language": "语言", 33 | "app.messages.history": "历史", 34 | "app.messages.clearHistory": "清空历史", 35 | "app.messages.inputError.needName": "请输入名称", 36 | "app.messages.inputError.needCode": "请输入代码", 37 | "app.messages.inputError.needEnvironment": "请指明环境", 38 | "app.messages.inputError.needDockerImage": "请指明Docker镜像", 39 | "app.messages.inputError.inputErrorNeedExtension": "请指明一个扩展名(php, js...)", 40 | "app.messages.batchUpload": "批量上传", 41 | "app.messages.dropFilesHere": "将文件拖拽至此", 42 | "app.messages.update": "更新", 43 | "app.messages.create": "创建", 44 | "app.messages.fileExt": "文件扩展名", 45 | "app.messages.upload": "上传", 46 | "app.messages.status": "状态", 47 | "app.messages.deleteUploaded": "删除已上传的", 48 | "app.messages.chooseFunctionFiles": "选择函数文件", 49 | "app.messages.uploaded": "已上传", 50 | "app.messages.processing": "处理中", 51 | "app.messages.pending": "等待中", 52 | "app.messages.failed": "失败", 53 | "app.messages.filter": "过滤器", 54 | "app.messages.response": "响应", 55 | "app.messages.noFunctionFiles": "请选择至少一个文件", 56 | 57 | "app.containers.FunctionsListPage.header": "函数列表", 58 | "app.containers.FunctionsPage.header": "函数", 59 | "app.containers.FunctionsPage.function.delete.triggers": "此函数有关联的触发器,同时删除这些触发器吗?", 60 | 61 | "app.containers.FunctionListItem.httptriggers": "Http 触发器", 62 | "app.containers.FunctionListItem.kubewatchers": "Kube 监视器", 63 | 64 | "app.containers.EnvironmentsPage.header": "环境", 65 | 66 | "app.components.TriggerHttpForm.headerhttptrigger": "Http 触发器", 67 | "app.components.KubeWatcherForm.headerkubewatcher": "Kube 监视器", 68 | 69 | "dummy": "" 70 | } 71 | -------------------------------------------------------------------------------- /app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import conformsTo from 'lodash/conformsTo'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import isFunction from 'lodash/isFunction'; 4 | import isObject from 'lodash/isObject'; 5 | import isString from 'lodash/isString'; 6 | import invariant from 'invariant'; 7 | import warning from 'warning'; 8 | import createReducer from 'reducers'; 9 | 10 | /** 11 | * Validate the shape of redux store 12 | */ 13 | export function checkStore(store) { 14 | const shape = { 15 | dispatch: isFunction, 16 | subscribe: isFunction, 17 | getState: isFunction, 18 | replaceReducer: isFunction, 19 | runSaga: isFunction, 20 | asyncReducers: isObject, 21 | }; 22 | invariant( 23 | conformsTo(store, shape), 24 | '(app/utils...) asyncInjectors: Expected a valid redux store' 25 | ); 26 | } 27 | 28 | /** 29 | * Inject an asynchronously loaded reducer 30 | */ 31 | export function injectAsyncReducer(store, isValid) { 32 | return function injectReducer(name, asyncReducer) { 33 | if (!isValid) checkStore(store); 34 | 35 | invariant( 36 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 37 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 38 | ); 39 | 40 | if (Reflect.has(store.asyncReducers, name)) return; 41 | 42 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 43 | store.replaceReducer(createReducer(store.asyncReducers)); 44 | }; 45 | } 46 | 47 | /** 48 | * Inject an asynchronously loaded saga 49 | */ 50 | export function injectAsyncSagas(store, isValid) { 51 | return function injectSagas(sagas) { 52 | if (!isValid) checkStore(store); 53 | 54 | invariant( 55 | Array.isArray(sagas), 56 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 57 | ); 58 | 59 | warning( 60 | !isEmpty(sagas), 61 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 62 | ); 63 | 64 | sagas.map(store.runSaga); 65 | }; 66 | } 67 | 68 | /** 69 | * Helper for creating injectors 70 | */ 71 | export function getAsyncInjectors(store) { 72 | checkStore(store); 73 | 74 | return { 75 | injectReducer: injectAsyncReducer(store, true), 76 | injectSagas: injectAsyncSagas(store, true), 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /app/utils/confirm.js: -------------------------------------------------------------------------------- 1 | import Confirmation from 'components/Confirmation'; 2 | import { createConfirmation } from 'react-confirm'; 3 | 4 | const defaultConfirmation = createConfirmation(Confirmation); 5 | 6 | export function confirm(confirmation, options = {}) { 7 | return defaultConfirmation({ confirmation, ...options }); 8 | } 9 | -------------------------------------------------------------------------------- /app/utils/tprapi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by damien on 23/02/2017. 3 | */ 4 | import axios from 'axios'; 5 | 6 | const basePath = '/proxy/tpr/'; 7 | 8 | /** 9 | * Parses the JSON returned by a network request 10 | * 11 | * @param {object} response A response from a network request 12 | * 13 | * @return {object} The parsed JSOs from the request 14 | */ 15 | function parseJSON(response) { 16 | return response.data; 17 | } 18 | 19 | /** 20 | * Checks if a network request came back fine, and throws an error if not 21 | * 22 | * @param {object} response A response from a network request 23 | * 24 | * @return {object|undefined} Returns either the response, or throws an error 25 | */ 26 | function checkStatus(response) { 27 | if (response.status >= 200 && response.status < 300) { 28 | return response; 29 | } 30 | 31 | const error = new Error(response.statusText); 32 | error.response = response; 33 | throw error; 34 | } 35 | 36 | export function getBenchmarkConfigs() { 37 | return axios.get(`${basePath}benchmark/configs`) 38 | .then(checkStatus) 39 | .then(parseJSON); 40 | } 41 | export function getBenchmarkConfig(name) { 42 | return axios.get(`${basePath}benchmark/configs/${name}`) 43 | .then(checkStatus) 44 | .then(parseJSON); 45 | } 46 | export function removeBenchmarkConfig(config) { 47 | return axios.delete(`${basePath}benchmark/configs/${config.metadata.name}`) 48 | .then(checkStatus) 49 | .then(parseJSON); 50 | } 51 | export function updateBenchmarkConfig(config) { 52 | // TODO maybe remove version info 53 | return axios.put(`${basePath}benchmark/configs/${config.metadata.name}`, config) 54 | .then(checkStatus) 55 | .then(parseJSON); 56 | } 57 | export function createBenchmarkConfig(config) { 58 | return axios.post(`${basePath}benchmark/configs`, config) 59 | .then(checkStatus) 60 | .then(parseJSON); 61 | } 62 | 63 | export function getBenchmarkInstances() { 64 | return axios.get(`${basePath}benchmark/instances`) 65 | .then(checkStatus) 66 | .then(parseJSON); 67 | } 68 | export function getBenchmarkInstance(name) { 69 | return axios.get(`${basePath}benchmark/instances/${name}`) 70 | .then(checkStatus) 71 | .then(parseJSON); 72 | } 73 | export function removeBenchmarkInstance(instance) { 74 | return axios.delete(`${basePath}benchmark/instances/${instance.metadata.name}`) 75 | .then(checkStatus) 76 | .then(parseJSON); 77 | } 78 | export function updateBenchmarkInstance(instance) { 79 | // TODO maybe remove version info 80 | return axios.put(`${basePath}benchmark/instances/${instance.metadata.name}`, instance) 81 | .then(checkStatus) 82 | .then(parseJSON); 83 | } 84 | export function createBenchmarkInstance(instance) { 85 | return axios.post(`${basePath}benchmark/instances`, instance) 86 | .then(checkStatus) 87 | .then(parseJSON); 88 | } 89 | -------------------------------------------------------------------------------- /app/utils/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by damien on 24/02/2017. 3 | */ 4 | 5 | export function slug(str) { 6 | // remove accents, swap ñ for n, etc 7 | const from = 'ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;'; 8 | const to = 'aaaaaeeeeeiiiiooooouuuunc------'; 9 | 10 | let strClean = str.replace(/^\s+|\s+$/g, ''); // trim 11 | strClean = strClean.toLowerCase(); 12 | 13 | for (let i = 0, l = from.length; i < l; i += 1) { 14 | strClean = strClean.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)); 15 | } 16 | 17 | strClean = strClean.replace(/[^a-z0-9 -]/g, '') // remove invalid chars 18 | .replace(/\s+/g, '-') // collapse whitespace and replace by - 19 | .replace(/-+/g, '-'); // collapse dashes 20 | 21 | return strClean; 22 | } 23 | 24 | export function decodeBase64(s) { 25 | return atob(s); 26 | } 27 | 28 | export function encodeBase64(s) { 29 | return btoa(s); 30 | } 31 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.4.0 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | RUN docker/build.sh 6 | 7 | FROM nginx:1.13.8 8 | 9 | COPY docker/fission-ui.conf.template /etc/nginx/conf.d/default.conf.template 10 | COPY docker/run.sh /run.sh 11 | COPY --from=0 /app/build /fission-ui 12 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Fission-ui Deployment using docker 2 | 3 | Fission-ui is a single page application, so it only contains static files and depends on fission APIs 4 | on path `/proxy/{service}`. 5 | 6 | ## Deploy with other fission components in k8s cluster 7 | 8 | The easiest way is to deploy fission-ui as an micro-service with other fission services in fission namespace. 9 | 10 | After deploying fission services, create a fission-ui service and deployment: 11 | ``` 12 | $ kubectl create -f fission-ui.yaml 13 | ``` 14 | You can then access the ui via `node_ip:31319` or load balance. 15 | 16 | We use Nginx to serve the static files, which are complied by `npm run build` and piped into the project build folder. 17 | It also handles api requests to the fission services inside k8s. 18 | 19 | ## Deploy a standalone fission-ui server 20 | 21 | Modify the Nginx config `fission-ui.conf`, change the fission service name to your fission service ip or domain name. 22 | 23 | ## Other features 24 | You can add other features like https, authentication by modifying Nginx config file. 25 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run prebuild && npm run build 4 | -------------------------------------------------------------------------------- /docker/fission-ui.conf.template: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have a line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # HTTP configurations 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name localhost; 17 | 18 | # Always serve index.html for any request 19 | location / { 20 | # Set path 21 | root /fission-ui/; 22 | try_files $uri /index.html; 23 | } 24 | 25 | # Do not cache sw.js, required for offline-first updates. 26 | location /sw.js { 27 | add_header Cache-Control "no-cache"; 28 | proxy_cache_bypass $http_pragma; 29 | proxy_cache_revalidate on; 30 | expires off; 31 | access_log off; 32 | } 33 | 34 | location /proxy/controller/ { 35 | 36 | proxy_pass http://controller.${FISSION_NAMESPACE}/; 37 | proxy_http_version 1.1; 38 | proxy_set_header X-Forwarded-Proto https; 39 | proxy_set_header Upgrade $http_upgrade; 40 | proxy_set_header Connection 'upgrade'; 41 | proxy_set_header Host $host; 42 | proxy_cache_bypass $http_upgrade; 43 | } 44 | 45 | location /proxy/router/ { 46 | 47 | proxy_pass http://router.${FISSION_NAMESPACE}/; 48 | proxy_http_version 1.1; 49 | proxy_set_header X-Forwarded-Proto https; 50 | proxy_set_header Upgrade $http_upgrade; 51 | proxy_set_header Connection 'upgrade'; 52 | proxy_set_header Host $host; 53 | proxy_cache_bypass $http_upgrade; 54 | } 55 | 56 | location /proxy/tpr/benchmark/ { 57 | 58 | proxy_pass http://127.0.0.1:8001/apis/benchmark.fission.io/v1/namespaces/fission-benchmark/; 59 | proxy_http_version 1.1; 60 | proxy_set_header X-Forwarded-Proto https; 61 | proxy_set_header Upgrade $http_upgrade; 62 | proxy_set_header Connection 'upgrade'; 63 | proxy_cache_bypass $http_upgrade; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /docker/fission-ui.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: fission-ui 5 | namespace: fission 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | svc: fission-ui 12 | spec: 13 | containers: 14 | - name: nginx 15 | image: yqf3139/fission-ui:latest 16 | env: 17 | - name: FISSION_NAMESPACE 18 | value: fission 19 | command: ["bash"] 20 | args: ["/run.sh"] 21 | - name: kubectl-proxy 22 | image: lachlanevenson/k8s-kubectl 23 | args: ["proxy", "--port", "8001", "--address", "127.0.0.1"] 24 | 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: fission-ui 30 | namespace: fission 31 | labels: 32 | svc: fission-ui 33 | spec: 34 | type: NodePort 35 | ports: 36 | - port: 80 37 | targetPort: 80 38 | nodePort: 31319 39 | selector: 40 | svc: fission-ui 41 | -------------------------------------------------------------------------------- /docker/push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | user=$1 6 | tag=$2 7 | if [ -z "$user" ] 8 | then 9 | user=fission 10 | fi 11 | if [ -z "$tag" ] 12 | then 13 | tag=latest 14 | fi 15 | 16 | docker build -t fission-ui -f docker/Dockerfile . 17 | docker tag fission-ui $user/fission-ui:$tag 18 | docker push $user/fission-ui:$tag 19 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | envsubst '${FISSION_NAMESPACE}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'; 4 | -------------------------------------------------------------------------------- /documentation/images/batch-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/documentation/images/batch-upload.png -------------------------------------------------------------------------------- /documentation/images/environment-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/documentation/images/environment-list.png -------------------------------------------------------------------------------- /documentation/images/function-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/documentation/images/function-edit.png -------------------------------------------------------------------------------- /documentation/images/function-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/documentation/images/function-list.png -------------------------------------------------------------------------------- /documentation/images/function-triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fission/fission-ui/d14ef875f905f9f33e6f76489a7f6bbfd562b0ff/documentation/images/function-triggers.png -------------------------------------------------------------------------------- /internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.4.0', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here. 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css', 31 | ], 32 | 33 | /** 34 | * Specify any additional dependencies here. We include core-js and lodash 35 | * since a lot of our dependencies depend on them and they get picked up by webpack. 36 | */ 37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 38 | 39 | // The path where the DLL manifest and bundle will get built 40 | path: resolve('../node_modules/react-boilerplate-dlls'), 41 | }, 42 | 43 | entry(pkg) { 44 | const dependencyNames = Object.keys(pkg.dependencies); 45 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 47 | const includeDependencies = uniq(dependencyNames.concat(include)); 48 | 49 | return { 50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 51 | }; 52 | }, 53 | }, 54 | }; 55 | 56 | module.exports = ReactBoilerplate; 57 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | 10 | {{#if wantMessages}} 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | {{/if}} 14 | 15 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 |
    19 | {{#if wantMessages}} 20 | 21 | {{/if}} 22 |
    23 | ); 24 | } 25 | } 26 | 27 | {{ properCase name }}.propTypes = { 28 | 29 | }; 30 | 31 | export default {{ properCase name }}; 32 | -------------------------------------------------------------------------------- /internals/generators/component/es6.pure.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | 10 | {{#if wantMessages}} 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | {{/if}} 14 | 15 | class {{ properCase name }} extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 |
    19 | {{#if wantMessages}} 20 | 21 | {{/if}} 22 |
    23 | ); 24 | } 25 | } 26 | 27 | {{ properCase name }}.propTypes = { 28 | 29 | }; 30 | 31 | export default {{ properCase name }}; 32 | -------------------------------------------------------------------------------- /internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | /* eslint strict: [0] */ 6 | 7 | 'use strict'; 8 | 9 | const componentExists = require('../utils/componentExists'); 10 | 11 | module.exports = { 12 | description: 'Add an unconnected component', 13 | prompts: [{ 14 | type: 'list', 15 | name: 'type', 16 | message: 'Select the type of component', 17 | default: 'Stateless Function', 18 | choices: () => ['Stateless Function', 'ES6 Class (Pure)', 'ES6 Class'], 19 | }, { 20 | type: 'input', 21 | name: 'name', 22 | message: 'What should it be called?', 23 | default: 'Button', 24 | validate: (value) => { 25 | if ((/.+/).test(value)) { 26 | return componentExists(value) ? 'A component or container with this name already exists' : true; 27 | } 28 | 29 | return 'The name is required'; 30 | }, 31 | }, { 32 | type: 'confirm', 33 | name: 'wantMessages', 34 | default: true, 35 | message: 'Do you want i18n messages (i.e. will this component use text)?', 36 | }], 37 | actions: (data) => { 38 | // Generate index.js and index.test.js 39 | let componentTemplate; 40 | 41 | switch (data.type) { 42 | case 'ES6 Class': { 43 | componentTemplate = './component/es6.js.hbs'; 44 | break; 45 | } 46 | case 'ES6 Class (Pure)': { 47 | componentTemplate = './component/es6.pure.js.hbs'; 48 | break; 49 | } 50 | case 'Stateless Function': { 51 | componentTemplate = './component/stateless.js.hbs'; 52 | break; 53 | } 54 | default: { 55 | componentTemplate = './component/es6.js.hbs'; 56 | } 57 | } 58 | 59 | const actions = [{ 60 | type: 'add', 61 | path: '../../app/components/{{properCase name}}/index.js', 62 | templateFile: componentTemplate, 63 | abortOnFail: true, 64 | }, { 65 | type: 'add', 66 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 67 | templateFile: './component/test.js.hbs', 68 | abortOnFail: true, 69 | }]; 70 | 71 | // If they want a i18n messages file 72 | if (data.wantMessages) { 73 | actions.push({ 74 | type: 'add', 75 | path: '../../app/components/{{properCase name}}/messages.js', 76 | templateFile: './component/messages.js.hbs', 77 | abortOnFail: true, 78 | }); 79 | } 80 | 81 | return actions; 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import styled from 'styled-components'; 9 | 10 | {{#if wantMessages}} 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | {{/if}} 14 | 15 | function {{ properCase name }}() { 16 | return ( 17 |
    18 | {{#if wantMessages}} 19 | 20 | {{/if}} 21 |
    22 | ); 23 | } 24 | 25 | {{ properCase name }}.propTypes = { 26 | 27 | }; 28 | 29 | export default {{ properCase name }}; 30 | -------------------------------------------------------------------------------- /internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import {{ properCase name }} from '../index'; 5 | 6 | describe('<{{ properCase name }} />', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { connect } from 'react-redux'; 9 | {{#if wantHeaders}} 10 | import Helmet from 'react-helmet'; 11 | {{/if}} 12 | {{#if wantMessages}} 13 | import { FormattedMessage } from 'react-intl'; 14 | {{/if}} 15 | {{#if wantActionsAndReducer}} 16 | import { createStructuredSelector } from 'reselect'; 17 | import makeSelect{{properCase name}} from './selectors'; 18 | {{/if}} 19 | {{#if wantMessages}} 20 | import messages from './messages'; 21 | {{/if}} 22 | 23 | export class {{ properCase name }} extends React.{{{ component }}} { // eslint-disable-line react/prefer-stateless-function 24 | render() { 25 | return ( 26 |
    27 | {{#if wantHeaders}} 28 | 34 | {{/if}} 35 | {{#if wantMessages}} 36 | 37 | {{/if}} 38 |
    39 | ); 40 | } 41 | } 42 | 43 | {{ properCase name }}.propTypes = { 44 | dispatch: PropTypes.func.isRequired, 45 | }; 46 | 47 | {{#if wantActionsAndReducer}} 48 | const mapStateToProps = createStructuredSelector({ 49 | {{name}}: makeSelect{{properCase name}}(), 50 | }); 51 | {{/if}} 52 | 53 | function mapDispatchToProps(dispatch) { 54 | return { 55 | dispatch, 56 | }; 57 | } 58 | 59 | {{#if wantActionsAndReducer}} 60 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 61 | {{else}} 62 | export default connect(null, mapDispatchToProps)({{ properCase name }}); 63 | {{/if}} 64 | -------------------------------------------------------------------------------- /internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { fromJS } from 'immutable'; 3 | import {{ camelCase name }}Reducer from '../reducer'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | // See example in containers/HomePage/sagas.js 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => (state) => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const makeSelect{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default makeSelect{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { makeSelect{{ properCase name }}Domain } from '../selectors'; 3 | 4 | // const selector = makeSelect{{ properCase name}}Domain(); 5 | 6 | describe('makeSelect{{ properCase name }}Domain', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { {{ properCase name }} } from '../index'; 5 | 6 | describe('<{{ properCase name }} />', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const componentGenerator = require('./component/index.js'); 10 | const containerGenerator = require('./container/index.js'); 11 | const routeGenerator = require('./route/index.js'); 12 | const languageGenerator = require('./language/index.js'); 13 | 14 | module.exports = (plop) => { 15 | plop.setGenerator('component', componentGenerator); 16 | plop.setGenerator('container', containerGenerator); 17 | plop.setGenerator('route', routeGenerator); 18 | plop.setGenerator('language', languageGenerator); 19 | plop.addHelper('directory', (comp) => { 20 | try { 21 | fs.accessSync(path.join(__dirname, `../../app/containers/${comp}`), fs.F_OK); 22 | return `containers/${comp}`; 23 | } catch (e) { 24 | return `components/${comp}`; 25 | } 26 | }); 27 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 28 | }; 29 | -------------------------------------------------------------------------------- /internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 '{{language}}', 2 | -------------------------------------------------------------------------------- /internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const componentExists = require('../utils/componentExists'); 7 | 8 | function reducerExists(comp) { 9 | try { 10 | fs.accessSync(path.join(__dirname, `../../../app/containers/${comp}/reducer.js`), fs.F_OK); 11 | return true; 12 | } catch (e) { 13 | return false; 14 | } 15 | } 16 | 17 | function sagasExists(comp) { 18 | try { 19 | fs.accessSync(path.join(__dirname, `../../../app/containers/${comp}/sagas.js`), fs.F_OK); 20 | return true; 21 | } catch (e) { 22 | return false; 23 | } 24 | } 25 | 26 | function trimTemplateFile(template) { 27 | // Loads the template file and trims the whitespace and then returns the content as a string. 28 | return fs.readFileSync(path.join(__dirname, `./${template}`), 'utf8').replace(/\s*$/, ''); 29 | } 30 | 31 | module.exports = { 32 | description: 'Add a route', 33 | prompts: [{ 34 | type: 'input', 35 | name: 'component', 36 | message: 'Which component should the route show?', 37 | validate: (value) => { 38 | if ((/.+/).test(value)) { 39 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 40 | } 41 | 42 | return 'The path is required'; 43 | }, 44 | }, { 45 | type: 'input', 46 | name: 'path', 47 | message: 'Enter the path of the route.', 48 | default: '/about', 49 | validate: (value) => { 50 | if ((/.+/).test(value)) { 51 | return true; 52 | } 53 | 54 | return 'path is required'; 55 | }, 56 | }], 57 | 58 | // Add the route to the routes.js file above the error route 59 | // TODO smarter route adding 60 | actions: (data) => { 61 | const actions = []; 62 | if (reducerExists(data.component)) { 63 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 64 | actions.push({ 65 | type: 'modify', 66 | path: '../../app/routes.js', 67 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 68 | template: trimTemplateFile('routeWithReducer.hbs'), 69 | }); 70 | } else { 71 | actions.push({ 72 | type: 'modify', 73 | path: '../../app/routes.js', 74 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 75 | template: trimTemplateFile('route.hbs'), 76 | }); 77 | } 78 | 79 | return actions; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const pageComponents = fs.readdirSync(path.join(__dirname, '../../../app/components')); 10 | const pageContainers = fs.readdirSync(path.join(__dirname, '../../../app/containers')); 11 | const components = pageComponents.concat(pageContainers); 12 | 13 | function componentExists(comp) { 14 | return components.indexOf(comp) >= 0; 15 | } 16 | 17 | module.exports = componentExists; 18 | -------------------------------------------------------------------------------- /internals/mocks/cssModule.js: -------------------------------------------------------------------------------- 1 | module.exports = 'CSS_MODULE'; 2 | -------------------------------------------------------------------------------- /internals/mocks/image.js: -------------------------------------------------------------------------------- 1 | module.exports = 'IMAGE_MOCK'; 2 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shelljs = require('shelljs'); 4 | const animateProgress = require('./helpers/progress'); 5 | const chalk = require('chalk'); 6 | const addCheckMark = require('./helpers/checkmark'); 7 | 8 | const progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback) // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | const addCheckMark = require('./helpers/checkmark.js'); 3 | 4 | if (!which('git')) { 5 | echo('Sorry, this script requires git'); 6 | exit(1); 7 | } 8 | 9 | if (!test('-e', 'internals/templates')) { 10 | echo('The example is deleted already.'); 11 | exit(1); 12 | } 13 | 14 | process.stdout.write('Cleanup started...'); 15 | 16 | // Reuse existing LanguageProvider and i18n tests 17 | mv('app/containers/LanguageProvider/tests', 'internals/templates/containers/LanguageProvider'); 18 | cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js'); 19 | 20 | // Cleanup components/ 21 | rm('-rf', 'app/components/*'); 22 | 23 | // Handle containers/ 24 | rm('-rf', 'app/containers'); 25 | mv('internals/templates/containers', 'app'); 26 | 27 | // Handle tests/ 28 | mv('internals/templates/tests', 'app'); 29 | 30 | // Handle translations/ 31 | rm('-rf', 'app/translations') 32 | mv('internals/templates/translations', 'app'); 33 | 34 | // Handle utils/ 35 | rm('-rf', 'app/utils'); 36 | mv('internals/templates/utils', 'app') 37 | 38 | // Replace the files in the root app/ folder 39 | cp('internals/templates/app.js', 'app/app.js'); 40 | cp('internals/templates/global-styles.js', 'app/global-styles.js'); 41 | cp('internals/templates/i18n.js', 'app/i18n.js'); 42 | cp('internals/templates/index.html', 'app/index.html'); 43 | cp('internals/templates/reducers.js', 'app/reducers.js'); 44 | cp('internals/templates/routes.js', 'app/routes.js'); 45 | cp('internals/templates/store.js', 'app/store.js'); 46 | 47 | // Remove the templates folder 48 | rm('-rf', 'internals/templates'); 49 | 50 | addCheckMark(); 51 | 52 | // Commit the changes 53 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 54 | echo('\nError: Git commit failed'); 55 | exit(1); 56 | } 57 | 58 | echo('\nCleanup done. Happy Coding!!!'); 59 | -------------------------------------------------------------------------------- /internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | // No need to build the DLL in production 2 | if (process.env.NODE_ENV === 'production') { 3 | process.exit(0); 4 | } 5 | 6 | require('shelljs/global'); 7 | 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | const exists = fs.existsSync; 11 | const writeFile = fs.writeFileSync; 12 | 13 | const defaults = require('lodash/defaultsDeep'); 14 | const pkg = require(path.join(process.cwd(), 'package.json')); 15 | const config = require('../config'); 16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults); 17 | const outputPath = path.join(process.cwd(), dllConfig.path); 18 | const dllManifestPath = path.join(outputPath, 'package.json'); 19 | 20 | /** 21 | * I use node_modules/react-boilerplate-dlls by default just because 22 | * it isn't going to be version controlled and babel wont try to parse it. 23 | */ 24 | mkdir('-p', outputPath); 25 | 26 | echo('Building the Webpack DLL...'); 27 | 28 | /** 29 | * Create a manifest so npm install doesn't warn us 30 | */ 31 | if (!exists(dllManifestPath)) { 32 | writeFile( 33 | dllManifestPath, 34 | JSON.stringify(defaults({ 35 | name: 'react-boilerplate-dlls', 36 | private: true, 37 | author: pkg.author, 38 | repository: pkg.repository, 39 | version: pkg.version, 40 | }), null, 2), 41 | 'utf8' 42 | ); 43 | } 44 | 45 | // the BUILDING_DLL env var is set to avoid confusing the development environment 46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js'); 47 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const readline = require('readline'); 4 | 5 | /** 6 | * Adds an animated progress indicator 7 | * 8 | * @param {string} message The message to write next to the indicator 9 | * @param {number} amountOfDots The amount of dots you want to animate 10 | */ 11 | function animateProgress(message, amountOfDots) { 12 | if (typeof amountOfDots !== 'number') { 13 | amountOfDots = 3; 14 | } 15 | 16 | let i = 0; 17 | return setInterval(function() { 18 | readline.cursorTo(process.stdout, 0); 19 | i = (i + 1) % (amountOfDots + 1); 20 | const dots = new Array(i + 1).join('.'); 21 | process.stdout.write(message + dots); 22 | }, 500); 23 | } 24 | 25 | module.exports = animateProgress; 26 | -------------------------------------------------------------------------------- /internals/scripts/helpers/xmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark cross symbol 5 | */ 6 | function addXMark(callback) { 7 | process.stdout.write(chalk.red(' ✘')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addXMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | exec('npm -v', function (err, stdout, stderr) { 3 | if (err) throw err; 4 | if (parseFloat(stdout) < 3) { 5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 6 | process.exit(1); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | -------------------------------------------------------------------------------- /internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel-loader', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Do not transform vendor's CSS with CSS-modules 22 | // The point is that they remain in global scope. 23 | // Since we require these CSS files in our JS or CSS files, 24 | // they will be a part of our compilation either way. 25 | // So, no need for ExtractTextPlugin here. 26 | test: /\.css$/, 27 | include: /node_modules/, 28 | loaders: ['style-loader', 'css-loader'], 29 | }, { 30 | test: /\.(eot|svg|ttf|woff|woff2)$/, 31 | loader: 'file-loader', 32 | }, { 33 | test: /\.(jpg|png|gif)$/, 34 | loaders: [ 35 | 'file-loader', 36 | { 37 | loader: 'image-webpack-loader', 38 | query: { 39 | progressive: true, 40 | optimizationLevel: 7, 41 | interlaced: false, 42 | pngquant: { 43 | quality: '65-90', 44 | speed: 4, 45 | }, 46 | }, 47 | }, 48 | ], 49 | }, { 50 | test: /\.html$/, 51 | loader: 'html-loader', 52 | }, { 53 | test: /\.json$/, 54 | loader: 'json-loader', 55 | }, { 56 | test: /\.(mp4|webm)$/, 57 | loader: 'url-loader', 58 | query: { 59 | limit: 10000, 60 | }, 61 | }], 62 | }, 63 | plugins: options.plugins.concat([ 64 | new webpack.ProvidePlugin({ 65 | // make fetch available 66 | fetch: 'exports-loader?self.fetch!whatwg-fetch', 67 | }), 68 | 69 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 70 | // inside your code for any environment checks; UglifyJS will automatically 71 | // drop any unreachable code. 72 | new webpack.DefinePlugin({ 73 | 'process.env': { 74 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 75 | }, 76 | }), 77 | new webpack.NamedModulesPlugin(), 78 | ]), 79 | resolve: { 80 | modules: ['app', 'node_modules'], 81 | extensions: [ 82 | '.js', 83 | '.jsx', 84 | '.react.js', 85 | ], 86 | mainFields: [ 87 | 'browser', 88 | 'jsnext:main', 89 | 'main', 90 | ], 91 | }, 92 | devtool: options.devtool, 93 | target: 'web', // Make web variables accessible to webpack, e.g. window 94 | performance: options.performance || {}, 95 | }); 96 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const dllPlugin = require('../config').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = require('./webpack.base.babel')({ 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 33 | ], 34 | performance: { 35 | hints: false, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const OfflinePlugin = require('offline-plugin'); 6 | 7 | module.exports = require('./webpack.base.babel')({ 8 | // In production, we skip all hot-reloading stuff 9 | entry: [ 10 | path.join(process.cwd(), 'app/app.js'), 11 | ], 12 | 13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 14 | output: { 15 | filename: '[name].[chunkhash].js', 16 | chunkFilename: '[name].[chunkhash].chunk.js', 17 | }, 18 | 19 | plugins: [ 20 | new webpack.optimize.CommonsChunkPlugin({ 21 | name: 'vendor', 22 | children: true, 23 | minChunks: 2, 24 | async: true, 25 | }), 26 | 27 | // Minify and optimize the index.html 28 | new HtmlWebpackPlugin({ 29 | template: 'app/index.html', 30 | minify: { 31 | removeComments: true, 32 | collapseWhitespace: true, 33 | removeRedundantAttributes: true, 34 | useShortDoctype: true, 35 | removeEmptyAttributes: true, 36 | removeStyleLinkTypeAttributes: true, 37 | keepClosingSlash: true, 38 | minifyJS: true, 39 | minifyCSS: true, 40 | minifyURLs: true, 41 | }, 42 | inject: true, 43 | }), 44 | 45 | // Put it in the end to capture all the HtmlWebpackPlugin's 46 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 47 | new OfflinePlugin({ 48 | relativePaths: false, 49 | publicPath: '/', 50 | 51 | // No need to cache .htaccess. See http://mxs.is/googmp, 52 | // this is applied before any match in `caches` section 53 | excludes: ['.htaccess'], 54 | 55 | caches: { 56 | main: [':rest:'], 57 | 58 | // All chunks marked as `additional`, loaded after main section 59 | // and do not prevent SW to install. Change to `optional` if 60 | // do not want them to be preloaded at all (cached only when first loaded) 61 | additional: ['*.chunk.js'], 62 | }, 63 | 64 | // Removes warning for about `additional` section usage 65 | safeToUseOptionalCaches: true, 66 | 67 | AppCache: false, 68 | }), 69 | ], 70 | 71 | performance: { 72 | assetFilter: (assetFilename) => !(/(\.map$)|(^(main\.|favicon\.))/.test(assetFilename)), 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const proxy = require('http-proxy-middleware'); 12 | const app = express(); 13 | const appInfluxdb = express(); 14 | 15 | let controllerBackend = '192.168.99.100:31313'; 16 | if (process.env.FISSION_CONTROLLER !== undefined) { 17 | controllerBackend = process.env.FISSION_CONTROLLER; 18 | } 19 | 20 | let routerBackend = '192.168.99.100:31314'; 21 | if (process.env.FISSION_ROUTER !== undefined) { 22 | routerBackend = process.env.FISSION_ROUTER; 23 | } 24 | 25 | let k8sBackend = '127.0.0.1:28001'; 26 | if (process.env.FISSION_K8S !== undefined) { 27 | k8sBackend = process.env.FISSION_K8S; 28 | } 29 | 30 | let influxdbBackend = '192.168.99.100:31315'; 31 | if (process.env.FISSION_LOGDB !== undefined) { 32 | influxdbBackend = process.env.FISSION_LOGDB; 33 | } 34 | 35 | // Setup proxy for fission APIs 36 | app.use('/proxy/controller', proxy({ target: `http://${controllerBackend}`, pathRewrite: { '^/proxy/controller': '' }, changeOrigin: true })); 37 | app.use('/proxy/router', proxy({ target: `http://${routerBackend}`, pathRewrite: { '^/proxy/router': '' }, changeOrigin: true })); 38 | app.use('/proxy/tpr/benchmark', proxy({ 39 | target: `http://${k8sBackend}`, 40 | pathRewrite: { '^/proxy/tpr/benchmark': '/apis/benchmark.fission.io/v1/namespaces/fission-benchmark' }, 41 | changeOrigin: true, 42 | })); 43 | appInfluxdb.use('', proxy({ target: `http://${influxdbBackend}`, changeOrigin: true })); 44 | 45 | // In production we need to pass these values in instead of relying on webpack 46 | setup(app, { 47 | outputPath: resolve(process.cwd(), 'build'), 48 | publicPath: '/', 49 | }); 50 | setup(appInfluxdb, { 51 | outputPath: resolve(process.cwd(), 'build'), 52 | publicPath: '/', 53 | }); 54 | 55 | 56 | // get the intended host and port number, use localhost and port 3000 if not provided 57 | const customHost = argv.host || process.env.HOST; 58 | const host = customHost || null; // Let http.Server use its default IPv6/4 host 59 | const prettyHost = customHost || 'localhost'; 60 | 61 | const port = argv.port || process.env.PORT || 3000; 62 | 63 | // Start your app. 64 | app.listen(port, host, (err) => { 65 | if (err) { 66 | return logger.error(err.message); 67 | } 68 | 69 | // Connect to ngrok in dev mode 70 | if (ngrok) { 71 | ngrok.connect(port, (innerErr, url) => { 72 | if (innerErr) { 73 | return logger.error(innerErr); 74 | } 75 | 76 | logger.appStarted(port, prettyHost, url); 77 | }); 78 | } else { 79 | logger.appStarted(port, prettyHost); 80 | } 81 | }); 82 | 83 | appInfluxdb.listen(31315, host, (err) => { 84 | if (err) { 85 | return logger.error(err.message); 86 | } 87 | 88 | logger.appStarted(31315, prettyHost); 89 | }); 90 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: (err) => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, host, tunnelStarted) => { 20 | console.log(`Server started ! ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://${host}:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | --------------------------------------------------------------------------------