├── .ackrc ├── .github └── workflows │ ├── build-release.yaml │ └── pr-build-check.yaml ├── .gitignore ├── LICENSE.txt ├── README.md ├── index.html.tmpl ├── package.json ├── partials ├── column-links.hbs └── field.hbs ├── scripts ├── build ├── clean ├── docker-patch └── upload ├── src ├── Cookie.js ├── Explorer.js ├── HTMLApi.js ├── URLParse.js ├── init.js └── template.js ├── styles ├── explorer.scss └── main.scss ├── templates ├── body.hbs ├── column-collection.hbs ├── column-resource.hbs ├── column.hbs ├── edit.hbs ├── explorer.hbs ├── filter.hbs ├── filters.hbs ├── modal.hbs ├── request.hbs ├── response.hbs └── test.hbs ├── vendor ├── JSONFormatter.js ├── async.js ├── jquery.scrollintoview.js ├── json2.js └── polyfill.js ├── version.json.tmpl └── yarn.lock /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-directory=is:node_modules 2 | --ignore-directory=is:compiled 3 | --type-add=json:ext:json 4 | --type-add=smarty:ext:tpl,hbs 5 | --smart-case 6 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yaml: -------------------------------------------------------------------------------- 1 | name: Build api-ui (Release) 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | env: 8 | CI_BUILD_TAG: ${{github.ref_name}} 9 | REPO: ${{github.event.repository.name || ''}} 10 | 11 | jobs: 12 | build-and-upload-release: 13 | name: Build & Upload Release 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: 'read' 17 | id-token: 'write' 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 1 22 | 23 | - id: build 24 | name: Build 25 | run: ./scripts/build 26 | 27 | - name: Get gcs auth 28 | uses: rancher-eio/read-vault-secrets@main 29 | with: 30 | secrets: | 31 | secret/data/github/repo/${{ github.repository }}/google-auth/rancher/credentials token | GOOGLE_AUTH 32 | 33 | - name: Apply gcs auth 34 | # https://github.com/google-github-actions/auth 35 | uses: 'google-github-actions/auth@v2' 36 | with: 37 | credentials_json: "${{ env.GOOGLE_AUTH }}" 38 | 39 | - name: Upload build 40 | uses: 'google-github-actions/upload-cloud-storage@v2' 41 | with: 42 | path: dist/${{steps.build.outputs.VERSION}} 43 | # Example - https://releases.rancher.com/ui/2.8.0.tar.gz 44 | destination: releases.rancher.com/${{ env.REPO }}/${{steps.build.outputs.VERSION}} 45 | parent: false 46 | headers: |- 47 | cache-control: no-cache,must-revalidate 48 | process_gcloudignore: false 49 | 50 | - name: Upload tar 51 | uses: 'google-github-actions/upload-cloud-storage@v2' 52 | # https://github.com/google-github-actions/upload-cloud-storage 53 | with: 54 | path: dist/${{steps.build.outputs.VERSION}}.tar.gz 55 | # Example - https://releases.rancher.com/ui/2.8.0/... 56 | destination: releases.rancher.com/${{ env.REPO }} 57 | parent: false 58 | headers: |- 59 | cache-control: no-cache,must-revalidate 60 | process_gcloudignore: false 61 | -------------------------------------------------------------------------------- /.github/workflows/pr-build-check.yaml: -------------------------------------------------------------------------------- 1 | name: check-build 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '16.x' 17 | - name: Validate build 18 | run: | 19 | yarn install 20 | yarn run build 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | tmp 6 | .*.sw? 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-ui 2 | 3 | An embedded UI for any service that implements the Rancher API spec. 4 | 5 | Integrating with your API 6 | ------- 7 | See [HTML UI](https://github.com/rancherio/api-spec/blob/master/specification.md#html-ui) in the API specification. This also includes a link to the latest version hosted on our CDN. 8 | 9 | ## Install 10 | 11 | ```bash 12 | git clone https://github.com/rancherio/api-ui 13 | cd api-ui 14 | yarn install 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### Compiling into stand-alone CSS and JavaScript files 20 | This will write files to `./dist/{version}/`, suitable for publishing to a CDN. 21 | 22 | ```bash 23 | ./scripts/build 24 | ``` 25 | 26 | ### Integrating with an API 27 | 28 | Wrap JSON responses with a bit of HTML (and return `Content-Type: text/html`): 29 | ```html 30 | 31 | 33 | 34 | 35 | 42 | ``` 43 | 44 | ### Options 45 | 46 | Several options can be configured through additional globals: 47 | 48 | ```javascript 49 | // Adds a documentation link in the navigation area 50 | var docsPage = "http://url-to-your-docs/site"; 51 | 52 | // URL to a documentation JSON file to add descriptions for types and fields. 53 | var docsJson = "http://url-to-your-docs.json"; 54 | 55 | // Displays the username who is logged in next to the Log Out link so the user knows who you think they are 56 | var user = "jsmith"; 57 | 58 | // Disables the display of the logout link 59 | var logout = false; // Disable the display of the Log Out link 60 | 61 | // Replaces the default "${API_ACCESS_KEY}:${API_SECRET_KEY}" string when displaying cURL commands. 62 | // setting to false will omit the user/pass option from the command entirely. 63 | var curlUser = "some:thing"; 64 | 65 | // Overrides the location where bootstrap is loaded from ('/css/boostrap.min.css' and '/js/bootstrap.min.js' will be appended to this) 66 | var bootstrap = "http://url/to/bootstrap/version"; 67 | ``` 68 | 69 | ## Bugs & Issues 70 | Please submit bugs and issues to [rancher/dashboard](//github.com/rancher/dashboard/issues) with a title starting with `[API UI] `. 71 | 72 | Or just [click here](//github.com/rancher/dashboard/issues/new?title=%5BAPI%20UI%5D%20) to create a new issue. 73 | 74 | ## License 75 | 76 | Copyright (c) 2014-2024 [Rancher Labs, Inc.](http://rancher.com) 77 | 78 | Licensed under the Apache License, Version 2.0 (the "License"); 79 | you may not use this file except in compliance with the License. 80 | You may obtain a copy of the License at 81 | 82 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 83 | 84 | Unless required by applicable law or agreed to in writing, software 85 | distributed under the License is distributed on an "AS IS" BASIS, 86 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 87 | See the License for the specific language governing permissions and 88 | limitations under the License. -------------------------------------------------------------------------------- /index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-ui", 3 | "version": "1.1.11", 4 | "description": "Embedded UI for any service that implements the Rancher API spec", 5 | "author": "SUSE", 6 | "license": "Apache-2.0", 7 | "homepage": "http://github.com/rancherio/api-ui", 8 | "bugs": "http://github.com/rancherio/dashboard/issues", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/rancherio/api-ui.git" 12 | }, 13 | "engine": { 14 | "node": ">= 14.14" 15 | }, 16 | "scripts": { 17 | "clean": "./scripts/clean", 18 | "build": "./scripts/build", 19 | "upload": "./scripts/upload" 20 | }, 21 | "devDependencies": { 22 | "clean-css-cli": "^5.6.2", 23 | "sass": "1.58.3", 24 | "uglify-js": "3.17.4" 25 | }, 26 | "dependencies": { 27 | "bootstrap": "3.4.1", 28 | "handlebars": "4.7.7", 29 | "jquery": "3.6.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /partials/column-links.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /partials/field.hbs: -------------------------------------------------------------------------------- 1 | {{#ifInList type 'array,map'}} 2 | {{#each children}} 3 |
4 | 5 | {{>field.hbs}} 6 |
7 | {{/each}} 8 | {{/ifInList}} 9 | 10 | {{#if field.required}} 11 | 12 | {{/if}} 13 | 14 | {{#if parentIsMap}} 15 | {{#if formFieldName2}} 16 |  :  17 | {{/if}} 18 | {{/if}} 19 | 20 | {{#ifInList type 'string,date,base64,dnsLabel,dnsLabelRestricted,hostname'}} 21 | 22 | {{/ifInList}} 23 | 24 | {{#ifEqual type 'json'}} 25 | 26 | {{/ifEqual}} 27 | 28 | {{#ifInList type 'password,masked'}} 29 | 30 | {{/ifInList}} 31 | 32 | {{#ifInList type 'number,int,float'}} 33 | 43 | {{/ifInList}} 44 | 45 | {{#ifEqual type "blob"}} 46 | 47 | {{/ifEqual}} 48 | 49 | {{#ifEqual type "boolean"}} 50 | {{#if field.nullable}} 51 | 56 | {{else}} 57 | 58 | {{/if}} 59 | {{/ifEqual}} 60 | 61 | {{#ifEqual type "enum"}} 62 | 70 | {{/ifEqual}} 71 | 72 | {{#ifEqual type "reference"}} 73 | {{#if field.options}} 74 | 86 | {{else}} 87 | 88 | {{/if}} 89 | {{/ifEqual}} 90 | 91 | {{#ifEqual type "__type__"}} 92 | 98 | {{/ifEqual}} 99 | 100 | {{#if field.required}} 101 | 102 | {{/if}} 103 | 104 | {{#if enlargeable}} 105 | 106 | {{/if}} 107 | 108 | {{#if nullCheck}} 109 |
110 | 113 |
114 | {{/if}} 115 | 116 | {{#unless addingField}} 117 | {{#ifInList type 'array,map'}} 118 | 119 | 120 | {{/ifInList}} 121 | {{/unless}} 122 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Building api-ui" 5 | 6 | rm -rf ./dist 7 | rm -rf ./tmp 8 | 9 | mkdir -p tmp/tpl 10 | 11 | HB_EXEC=./node_modules/.bin/handlebars 12 | 13 | VERSION=$(npm pkg get version | tr -d '"') 14 | echo "Version: ${VERSION}" 15 | echo "" 16 | 17 | echo "Installing NPM dependencies" 18 | npm install 19 | 20 | DIST=./dist/${VERSION} 21 | mkdir -p ${DIST} 22 | 23 | echo "Compiling templates ..." 24 | 25 | # Compile templates 26 | for FILE in ./templates/*; 27 | do 28 | NAME=$(basename $FILE) 29 | ${HB_EXEC} $FILE -f ./tmp/tpl/${NAME}.js 30 | done 31 | 32 | # Compile partials 33 | for FILE in ./partials/*; 34 | do 35 | NAME=$(basename $FILE) 36 | ${HB_EXEC} --partial $FILE -f ./tmp/tpl/${NAME}.js 37 | done 38 | 39 | echo "Building ..." 40 | 41 | JS_FILES=(node_modules/jquery/dist/jquery.js \ 42 | vendor/jquery.scrollintoview.js \ 43 | node_modules/bootstrap/dist/js/bootstrap.js \ 44 | vendor/async.js \ 45 | vendor/json2.js \ 46 | vendor/polyfill.js \ 47 | vendor/JSONFormatter.js \ 48 | src/URLParse.js \ 49 | src/Cookie.js \ 50 | node_modules/handlebars/dist/handlebars.runtime.js \ 51 | src/template.js \ 52 | tmp/tpl/** \ 53 | src/HTMLApi.js \ 54 | src/Explorer.js \ 55 | src/init.js) 56 | 57 | # Combine all javascript files together 58 | # Need to add a semicolon between each file 59 | for str in ${JS_FILES[@]}; do 60 | cat $str >> ${DIST}/ui.js 61 | echo ";" >> ${DIST}/ui.js 62 | done 63 | 64 | # Copy bootstrap files 65 | cp -R node_modules/bootstrap/dist/** ${DIST} 66 | 67 | # Patch bootstrap.css to remove source mapping URL comment 68 | sed -i.bak -e "s@/\*# sourceMappingURL=bootstrap\.css\.map.*@@g" ${DIST}/css/bootstrap.css 69 | rm -f ${DIST}/css/bootstrap.css.bak 70 | 71 | # Generate css 72 | cat ./node_modules/bootstrap/dist/css/bootstrap.css \ 73 | styles/main.scss \ 74 | styles/explorer.scss \ 75 | > ./tmp/ui.scss 76 | 77 | # Compile sass 78 | ./node_modules/.bin/sass ./tmp/ui.scss ${DIST}/ui.css 79 | 80 | # Minimise javascript 81 | ./node_modules/.bin/uglifyjs -c --source-map -o ${DIST}/ui.min.js ${DIST}/ui.js 82 | 83 | # Minimise css 84 | ./node_modules/.bin/cleancss ${DIST}/ui.css -o ${DIST}/ui.min.css 85 | 86 | # tar 87 | echo "Creating tar file ..." 88 | tar -cvzf ./dist/${VERSION}.tar.gz -C ${DIST} . 89 | 90 | # Copy index.html for testing 91 | sed -e "s/VERSION/"${VERSION}"/g" ./index.html.tmpl > ./dist/index.html 92 | 93 | # Generate versions file 94 | COMMIT=$(git rev-parse --short HEAD) 95 | sed -e "s/VERSION/"${VERSION}"/g" ./version.json.tmpl > ${DIST}/version.json 96 | sed -i.bak -e "s/COMMIT/"${COMMIT}"/g" ${DIST}/version.json 97 | rm -f ${DIST}/version.json.bak 98 | 99 | ENV_OUTPUT="${GITHUB_OUTPUT:-"temp-env"}" 100 | echo "VERSION=${VERSION}" >> "$ENV_OUTPUT" 101 | 102 | echo "" 103 | echo "All done" 104 | echo "" 105 | -------------------------------------------------------------------------------- /scripts/clean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | rm -rf ./dist 5 | rm -rf ./tmp 6 | -------------------------------------------------------------------------------- /scripts/docker-patch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use this script to patch the api-ui in a running container 4 | 5 | CONTAINER=$1 6 | 7 | if [ -z "$1" ]; then 8 | echo "Need container ID" 9 | exit 1 10 | fi 11 | 12 | if [ ! -d ./dist ]; then 13 | echo "You need to build first with 'yarn build'" 14 | exit 1 15 | fi 16 | 17 | echo "Patching running container with latest api-ui" 18 | 19 | docker exec -it ${CONTAINER} bash -c "rm -rf /usr/share/rancher/ui/api-ui/*" 20 | 21 | # Copy in the built code 22 | 23 | VERSION=$(node -e "(function () { console.log(require('./package.json').version) })()") 24 | 25 | docker cp ./dist/${VERSION}/ ${CONTAINER}:/usr/share/rancher 26 | 27 | docker exec -it ${CONTAINER} bash -c "cp -R /usr/share/rancher/${VERSION}/* /usr/share/rancher/ui/api-ui/" 28 | docker exec -it ${CONTAINER} bash -c "rm -rf /usr/share/rancher/${VERSION}" 29 | -------------------------------------------------------------------------------- /scripts/upload: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # cd to app root 5 | CWD=$(dirname $0) 6 | if [ `basename $(pwd)` = 'scripts' ]; then 7 | cd ../ 8 | else 9 | cd `dirname $CWD` 10 | fi 11 | 12 | function exec() { 13 | $@ 14 | if [ $? -ne 0 ]; then 15 | echo "Command: $@ failed" 16 | exit 2 17 | fi 18 | } 19 | 20 | VERSION=$(cat package.json | grep version | cut -f4 -d'"' | sed 's/-/_/g') 21 | BUILD_DIR="dist/${VERSION}" 22 | TARBALL="dist/${VERSION}.tar.gz" 23 | CDN="releases.rancher.com/api-ui" 24 | 25 | echo "Uploading..." 26 | exec gsutil -m cp -R -z "js,css,map" $BUILD_DIR "gs://${CDN}/" 27 | exec gsutil -m cp -R -z "js,css,map" $TARBALL "gs://${CDN}/" 28 | -------------------------------------------------------------------------------- /src/Cookie.js: -------------------------------------------------------------------------------- 1 | function Cookie() {}; 2 | 3 | Cookie.set = function(name,value,expire,path,domain,secure) 4 | { 5 | var expire_date; 6 | if ( typeof(expire) == 'object' ) 7 | expire_date = expire; 8 | else if ( expire ) 9 | expire_date = new Date( (new Date()).getTime() + (86400000 * expire)); 10 | 11 | var str = name +'=' + value.replace(/ /g,'%20').replace(/,/g,'%2C').replace(/;/g,'%3B'); 12 | 13 | if ( expire ) 14 | str += ';expires=' + expire_date.toGMTString(); 15 | 16 | if ( path ) 17 | str += ';path=' + path; 18 | 19 | if ( domain ) 20 | str += ';domain=' + domain; 21 | 22 | if ( secure ) 23 | str += ';secure'; 24 | 25 | try 26 | { 27 | document.cookie = str; 28 | } 29 | catch ( e ) 30 | { 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | Cookie.get = function(name) 37 | { 38 | var all = Cookie.getAll(); 39 | 40 | if ( typeof( all[name] ) == 'undefined' ) 41 | return false; 42 | 43 | return all[name]; 44 | } 45 | 46 | Cookie.getAll = function() 47 | { 48 | var cookies = document.cookie.split(/;\s*/); 49 | var tmp,name,val; 50 | var ret = {}; 51 | 52 | for ( var i = 0 ; i < cookies.length ; i++ ) 53 | { 54 | tmp = cookies[i].split(/=/); 55 | name = tmp[0].trim(); 56 | 57 | if ( !name ) 58 | continue; 59 | 60 | if ( tmp.length > 1 ) 61 | val = unescape(tmp[1].trim()); 62 | else 63 | val = ''; 64 | 65 | ret[ name ] = val; 66 | } 67 | 68 | return ret; 69 | } 70 | 71 | Cookie.remove = function(name) 72 | { 73 | document.cookie = name+'=null; expires=Wed, 24 Feb 1982 18:42:00 UTC'; 74 | } 75 | -------------------------------------------------------------------------------- /src/Explorer.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | 3 | var Explorer = function Explorer(htmlapi) 4 | { 5 | this._api = htmlapi; 6 | this._columns = []; 7 | this._lastColumnId = 0; 8 | 9 | this._lastClickColumn = null; 10 | this._lastClickRow = null; 11 | } 12 | 13 | Explorer.prototype.show = function(cb) 14 | { 15 | var tpl = { 16 | docs: this._api._docs, 17 | user: this._api._user 18 | }; 19 | 20 | document.body.innerHTML = Handlebars.templates['explorer.hbs'](tpl); 21 | $('#explorer').css('top', $('#header')[0].offsetHeight + 'px'); 22 | 23 | this.rootColumn(); 24 | 25 | if ( cb ) 26 | return async.nextTick(cb); 27 | } 28 | 29 | Explorer.prototype.rootColumn = function() 30 | { 31 | var data = []; 32 | var schemas = this._api._schemas; 33 | var keys = Object.keys(schemas); 34 | var k, schema; 35 | for ( var i = 0 ; i < keys.length ; i++ ) 36 | { 37 | k = keys[i]; 38 | schema = schemas[k]; 39 | if ( schema.links && schema.links.collection ) 40 | { 41 | data.push({ 42 | id: schema.id, 43 | displayName: schema.displayName || schema.name || schema.id, 44 | links: { 45 | 'self': schema.links.collection 46 | } 47 | }); 48 | } 49 | } 50 | 51 | var col = this.addPlaceholder(); 52 | this.populateColumn(col, { 53 | type: 'collection', 54 | data: data 55 | }); 56 | } 57 | 58 | Explorer.prototype.removeColumns = function(numOrElem, inclusive) 59 | { 60 | var stopAt; 61 | 62 | if ( inclusive !== true ) 63 | inclusive = false; 64 | 65 | if ( typeof numOrElem === 'object' ) 66 | stopAt = numOrElem.data('num'); 67 | else 68 | stopAt = numOrElem; 69 | 70 | if ( inclusive ) 71 | stopAt--; 72 | 73 | var col; 74 | while ( this._columns.length > stopAt ) 75 | { 76 | col = this._columns.pop(); 77 | col.remove(); 78 | } 79 | 80 | this.resizeWrapper(); 81 | } 82 | 83 | Explorer.prototype.addPlaceholder = function() 84 | { 85 | var id = ++this._lastColumnId; 86 | var tpl = { 87 | id: id, 88 | num: this._columns.length + 1 89 | }; 90 | 91 | var html = Handlebars.templates['column.hbs'](tpl); 92 | $('#explorer-wrapper').append(html); 93 | 94 | var elem = $('#column_'+ id); 95 | 96 | this._columns.push(elem); 97 | this.resizeWrapper(); 98 | elem.scrollintoview(); 99 | return elem; 100 | } 101 | 102 | Explorer.prototype._findColumnById = function(id) 103 | { 104 | var col; 105 | for ( var i = 0, len = this._columns.length ; i < len ; i++ ) 106 | { 107 | col = this._columns[id]; 108 | if ( col.data('id') == id ) 109 | { 110 | return col; 111 | } 112 | } 113 | 114 | return null; 115 | } 116 | 117 | Explorer.prototype._findColumnByChild = function(child) 118 | { 119 | return $(child).parents('.column'); 120 | } 121 | 122 | Explorer.prototype.populateColumn = function(idOrElem, obj) 123 | { 124 | var col; 125 | if ( typeof idOrElem === 'object' ) 126 | col = idOrElem; 127 | else 128 | col = this._findColumnById(id); 129 | 130 | if ( !col ) 131 | return; 132 | 133 | var links = obj.links || {}; 134 | delete obj.links; 135 | delete links.self; 136 | delete links.schemas; 137 | 138 | var hasLinks = false; 139 | for ( var k in links ) 140 | { 141 | hasLinks = true; 142 | break; 143 | } 144 | 145 | if ( obj.type && obj.type == 'collection' ) 146 | { 147 | var tpl = { 148 | id: col.data('id'), 149 | num: col.data('num'), 150 | pagination: obj.pagination, 151 | data: obj.data, 152 | links: links, 153 | hasLinks: hasLinks 154 | }; 155 | 156 | var html = Handlebars.templates['column-collection.hbs'](tpl); 157 | col.html(html); 158 | 159 | col.on('click', $.proxy(this.clickRow, this)); 160 | } 161 | else 162 | { 163 | var actions = obj.actions || {}; 164 | delete obj.actions; 165 | 166 | var tpl = { 167 | id: col.data('id'), 168 | num: col.data('num'), 169 | actions: actions, 170 | data: obj, 171 | links: links, 172 | hasLinks: hasLinks 173 | }; 174 | 175 | var html = Handlebars.templates['column-resource.hbs'](tpl); 176 | col.html(html); 177 | 178 | $('.column-links',col).on('click', $.proxy(this.followLink, this)); 179 | } 180 | 181 | this.resizeWrapper(); 182 | col.scrollintoview(); 183 | } 184 | 185 | var isMac = navigator.platform.toLowerCase().indexOf('mac') !== -1; 186 | function moreKey(event) 187 | { 188 | return (isMac ? event.metaKey : event.ctrlKey ); 189 | } 190 | 191 | function rangeKey(event) 192 | { 193 | return event.shiftKey; 194 | } 195 | 196 | function compareOrder(node1, node2) 197 | { 198 | if (node1 == node2) 199 | { 200 | return 0; 201 | } 202 | 203 | if (node1.compareDocumentPosition) 204 | { 205 | // 4 is the bitmask for FOLLOWS. 206 | // 2 is the bitmask for PRECEEDS. 207 | return node1.compareDocumentPosition(node2) & 2 ? 1 : -1; 208 | } 209 | 210 | // Process in IE using sourceIndex - we check to see if the first node has a source index or if it's parent has one. 211 | if ('sourceIndex' in node1 || (node1.parentNode && 'sourceIndex' in node1.parentNode)) 212 | { 213 | 214 | var isElement1 = node1.nodeType == 1; 215 | var isElement2 = node2.nodeType == 1; 216 | 217 | var index1 = isElement1 ? node1.sourceIndex : node1.parentNode.sourceIndex; 218 | var index2 = isElement2 ? node2.sourceIndex : node2.parentNode.sourceIndex; 219 | 220 | if (index1 != index2) 221 | { 222 | return ( index1 - index2 < 0 ? -1 : 1); 223 | } 224 | else 225 | { 226 | if (isElement1) 227 | { 228 | // Since they are not equal, we can deduce that node2 is a child of 229 | // node1 and therefore node1 comes first. 230 | return -1; 231 | } 232 | 233 | if (isElement2) 234 | { 235 | // Similarly, we know node1 is a child of node2 in this case. 236 | return 1; 237 | } 238 | 239 | // If we get here, we know node1 and node2 are both child nodes of the 240 | // same parent element. 241 | var s = node2; 242 | while ((s = s.previousSibling)) 243 | { 244 | if (s == node1) 245 | { 246 | // We just found node1 before node2. 247 | return -1; 248 | } 249 | } 250 | 251 | // Since we didn't find it, node1 must be after node2. 252 | return 1; 253 | } 254 | } 255 | 256 | // For Safari, we cheat and compare ranges. 257 | var doc = ( node1.nodeType == 9 ? node1 : node1.ownerDocument || node1.document ); 258 | 259 | var range1, range2, compare; 260 | range1 = doc.createRange(); 261 | range1.selectNode(node1); 262 | range1.collapse(true); 263 | 264 | range2 = doc.createRange(); 265 | range2.selectNode(node2); 266 | range2.collapse(true); 267 | 268 | var result = range1.compareBoundaryPoints(Range.START_TO_END, range2); 269 | return result; 270 | } 271 | 272 | Explorer.prototype.followLink = function(event) 273 | { 274 | var self = this; 275 | var $tgt = $(event.target); 276 | var link = $tgt.data('self'); 277 | 278 | // Open in new window with ctrl/apple key or middle click 279 | if ( link && ( moreKey(event) || event.which == 2 )) 280 | { 281 | window.open(link); 282 | } 283 | else 284 | { 285 | this.clickRow(event); 286 | } 287 | } 288 | 289 | Explorer.prototype.clickRow = function(event) 290 | { 291 | var self = this; 292 | var $tgt = $(event.target); 293 | 294 | if ( ! $tgt.hasClass('row') ) 295 | return; 296 | 297 | var $col = this._findColumnByChild($tgt); 298 | 299 | // Range & multiple selection 300 | if ( moreKey(event) ) 301 | { 302 | $tgt.toggleClass('selected'); 303 | } 304 | else if ( rangeKey(event) && $col[0] == this._lastClickColumn ) 305 | { 306 | var newState = !$tgt.hasClass('selected'); 307 | var cur,to; 308 | if ( compareOrder(this._lastClickRow, $tgt[0]) == -1 ) 309 | { 310 | cur = this._lastClickRow; 311 | to = $tgt[0]; 312 | } 313 | else 314 | { 315 | cur = $tgt[0]; 316 | to = this._lastClickRow; 317 | } 318 | 319 | $(cur).nextUntil(to).toggleClass('selected', newState); 320 | $(to).toggleClass('selected', newState); 321 | event.preventDefault(); 322 | event.stopPropagation(); 323 | } 324 | else 325 | { 326 | // Single selection 327 | $('LI.row.selected', $col).removeClass('selected'); 328 | $tgt.addClass('selected'); 329 | } 330 | 331 | // Remember the last click for next time 332 | this._lastClickColumn = $col[0]; 333 | this._lastClickRow = $tgt[0]; 334 | 335 | // Remove columns after this one 336 | this.removeColumns($col); 337 | 338 | var $sel = $('LI.row.selected', $col); 339 | if ( $sel.length == 0 ) 340 | { 341 | alert('Nothing is selected'); 342 | return; 343 | } 344 | else if ( $sel.length > 1 ) 345 | { 346 | alert($sel.length + ' things are selected'); 347 | return; 348 | } 349 | else 350 | { 351 | var link = $tgt.data('self'); 352 | var $newColumn = this.addPlaceholder(); 353 | this._api.ajax('GET', link, function(err,res) { 354 | if ( err ) 355 | { 356 | self.removeColumns($newColumn,true); 357 | alert(err); 358 | return; 359 | } 360 | 361 | self.populateColumn($newColumn, res); 362 | }); 363 | } 364 | } 365 | 366 | Explorer.prototype.resizeWrapper = function() 367 | { 368 | var total = 0; 369 | $.each(this._columns, function(k,v) { 370 | total += v.width()+1; 371 | }); 372 | 373 | $('#explorer-wrapper').width(total); 374 | } 375 | 376 | window.Explorer = Explorer; 377 | 378 | }(window)); 379 | -------------------------------------------------------------------------------- /src/HTMLApi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //data, schemasUrl, docs, user, curlUser, cb 4 | 5 | function HTMLApi(opt, cb) 6 | { 7 | var self = this; 8 | this._schemas = null; 9 | this._schemaDocs = null; 10 | this._data = opt.data; 11 | this._docsPage = opt.docsPage; 12 | this._docsJson = opt.docsJson; 13 | this._user = opt.user; 14 | this._curlUser = opt.curlUser || '${API_ACCESS_KEY}:${API_SECRET_KEY}'; 15 | this._logout = opt.logout !== false; 16 | 17 | this._filterId = 0; 18 | this._reqModal = null; 19 | this._editSchema = null; 20 | this._editData = null; 21 | this._lastMethod = null; 22 | this._lastMode = null; 23 | this._lastType = null; 24 | this._lastOpt = null; 25 | this._lastRequestBody = null; 26 | this._error = null; 27 | 28 | this._referenceDropdownLimit = 100; 29 | this._magicNull = "__-*NULL*-__"; 30 | this._magicNullRegex= new RegExp(this._escapeRegex(this._magicNull)+'$'); 31 | 32 | this._formatter = new JSONFormatter({ 33 | baseUrl: window.location.protocol +"//" + window.location.host, 34 | keyFormatter: this.keyFormatter.bind(this), 35 | valueFormatter: this.valueFormatter.bind(this) 36 | }); 37 | 38 | async.auto({ 39 | title: this.titleUpdate.bind(this), 40 | rawSchema: this.schemasLoad.bind(this, opt.schemasUrl), 41 | schema: ['rawSchema', this.schemasMunge.bind(this) ], 42 | docs: ['schema', this.docsLoad.bind(this, opt.docsJson) ], 43 | }, initDone); 44 | 45 | function initDone(err, results) 46 | { 47 | self._error = err; 48 | cb(); 49 | } 50 | } 51 | 52 | HTMLApi.prototype.show = function(cb) 53 | { 54 | var self = this; 55 | async.auto({ 56 | render: this.render.bind(this) , 57 | filters: ['render', this.filterInit.bind(this) ], 58 | }, showDone); 59 | 60 | function showDone(err, results) 61 | { 62 | if ( err ) 63 | { 64 | } 65 | 66 | if ( self._error ) 67 | { 68 | $('#header-body').css('display','none'); 69 | $('#header-error').css('display',''); 70 | } 71 | else 72 | { 73 | $('#header-body').css('visibility','visible'); 74 | } 75 | 76 | if ( cb ) 77 | cb(); 78 | } 79 | } 80 | 81 | HTMLApi.prototype.showModal = function(body,opt,cb) 82 | { 83 | var self = this; 84 | 85 | if ( !this.onKeys ) 86 | { 87 | this.onKeys = function(e) { 88 | if ( e.keyCode == 13 ) 89 | { 90 | // Find the first primary button and click it 91 | var actions = self._reqModal._actions; 92 | for (var i = 0 ; i < actions.length ; i++ ) 93 | { 94 | if ( actions[i].primary ) 95 | { 96 | self.modalAction(actions[i].id); 97 | return false; 98 | } 99 | } 100 | } 101 | else if ( e.keyCode == 27 ) 102 | { 103 | var actions = self._reqModal._actions; 104 | for (var i = 0 ; i < actions.length ; i++ ) 105 | { 106 | if ( actions[i].cancel ) 107 | { 108 | self.modalAction(actions[i].id); 109 | return false; 110 | } 111 | } 112 | } 113 | 114 | return true; 115 | }.bind(this); 116 | } 117 | 118 | 119 | this.hideModal(); 120 | 121 | if ( !body ) 122 | { 123 | body = '
'; 124 | } 125 | 126 | opt.body = body; 127 | 128 | var modalHtml = Handlebars.templates['modal.hbs'](opt); 129 | var modal = $(modalHtml); 130 | this._reqModal = modal; 131 | $('.modal-dialog',modal).css('width',opt.width||'750px'); 132 | this.setModalActions(opt.actions); 133 | modal.bind('keydown', this.onKeys); 134 | modal.modal({backdrop: 'static', keyboard: false}); 135 | 136 | if ( cb ) 137 | { 138 | modal.on('shown.bs.modal', function() { cb(modal); }); 139 | } 140 | 141 | } 142 | 143 | HTMLApi.prototype.replaceModal = function(html) 144 | { 145 | $('.modal-body', this._reqModal).html(html); 146 | } 147 | 148 | HTMLApi.prototype.modalAction = function(id) { 149 | var action; 150 | this._reqModal._actions.forEach(function(candidate) { 151 | if ( candidate.id == id ) 152 | action = candidate; 153 | }); 154 | 155 | if ( action && action.onClick) 156 | { 157 | action.onClick(); 158 | return false; 159 | } 160 | else if ( action && action.cancel ) 161 | { 162 | this.hideModal(); 163 | } 164 | } 165 | 166 | HTMLApi.prototype.hideModal = function() { 167 | var self = this; 168 | var old = self._reqModal; 169 | self._reqModal = null; 170 | 171 | if ( !old ) 172 | return; 173 | 174 | old.unbind('keydown', self.onKeys); 175 | old.modal('hide'); 176 | old.on('hidden.bs.modal', function() { 177 | old.remove(); 178 | }); 179 | } 180 | 181 | HTMLApi.prototype.setModalActions = function(actions) 182 | { 183 | actions = actions || []; 184 | this._reqModal._actions = actions; 185 | var html = ''; 186 | 187 | actions.forEach(function(action) { 188 | var color = 'btn-default'; 189 | var btnType = 'button' 190 | if ( action.primary ) { 191 | color = 'btn-primary'; 192 | btnType = 'submit'; 193 | } else if ( action.cancel ) { 194 | color = 'btn-link'; 195 | } 196 | 197 | html += ''; 198 | }); 199 | 200 | $('.modal-footer', this._reqModal).html(html); 201 | } 202 | 203 | HTMLApi.prototype.titleUpdate = function(cb) 204 | { 205 | var title = "API"; 206 | if ( this._data ) 207 | title += ": " + (this._data.displayName || this._data.name || this._data.id || this._data.resourceType || this._data.type); 208 | 209 | document.title = title; 210 | 211 | if ( cb ) 212 | async.nextTick(cb); 213 | } 214 | 215 | HTMLApi.prototype.schemasLoad = function(link, cb) 216 | { 217 | if ( !this._data ) 218 | return async.nextTick(function() { cb("No data") }); 219 | 220 | // Link may come from the page ' + 253 | '' + 254 | content + 255 | ''; 256 | }, 257 | 258 | // Click handler for collapsing and expanding objects and arrays 259 | collapse: function(evt) { 260 | var collapser = evt.target; 261 | 262 | var target = collapser.parentNode.getElementsByClassName('collapsible'); 263 | 264 | if ( ! target.length ) { 265 | return; 266 | } 267 | 268 | target = target[0]; 269 | 270 | if ( target.style.display == 'none' ) { 271 | var ellipsis = target.parentNode.getElementsByClassName('ellipsis')[0]; 272 | target.parentNode.removeChild(ellipsis); 273 | target.style.display = ''; 274 | $(collapser).removeClass('glyphicon-plus'); 275 | $(collapser).addClass('glyphicon-minus'); 276 | } else { 277 | target.style.display = 'none'; 278 | 279 | var ellipsis = document.createElement('span'); 280 | ellipsis.className = 'ellipsis'; 281 | ellipsis.innerHTML = ' … '; 282 | target.parentNode.insertBefore(ellipsis, target); 283 | $(collapser).removeClass('glyphicon-minus'); 284 | $(collapser).addClass('glyphicon-plus'); 285 | } 286 | } 287 | 288 | }; 289 | -------------------------------------------------------------------------------- /vendor/async.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * async v0.9.0 3 | * https://github.com/caolan/async 4 | * 5 | * Copyright 2010-2014 Caolan McMahon 6 | * Released under the MIT license 7 | */ 8 | /*jshint onevar: false, indent:4 */ 9 | /*global setImmediate: false, setTimeout: false, console: false */ 10 | (function () { 11 | 12 | var async = {}; 13 | 14 | // global on the server, window in the browser 15 | var root, previous_async; 16 | 17 | root = this; 18 | if (root != null) { 19 | previous_async = root.async; 20 | } 21 | 22 | async.noConflict = function () { 23 | root.async = previous_async; 24 | return async; 25 | }; 26 | 27 | function only_once(fn) { 28 | var called = false; 29 | return function() { 30 | if (called) throw new Error("Callback was already called."); 31 | called = true; 32 | fn.apply(root, arguments); 33 | } 34 | } 35 | 36 | //// cross-browser compatiblity functions //// 37 | 38 | var _toString = Object.prototype.toString; 39 | 40 | var _isArray = Array.isArray || function (obj) { 41 | return _toString.call(obj) === '[object Array]'; 42 | }; 43 | 44 | var _each = function (arr, iterator) { 45 | if (arr.forEach) { 46 | return arr.forEach(iterator); 47 | } 48 | for (var i = 0; i < arr.length; i += 1) { 49 | iterator(arr[i], i, arr); 50 | } 51 | }; 52 | 53 | var _map = function (arr, iterator) { 54 | if (arr.map) { 55 | return arr.map(iterator); 56 | } 57 | var results = []; 58 | _each(arr, function (x, i, a) { 59 | results.push(iterator(x, i, a)); 60 | }); 61 | return results; 62 | }; 63 | 64 | var _reduce = function (arr, iterator, memo) { 65 | if (arr.reduce) { 66 | return arr.reduce(iterator, memo); 67 | } 68 | _each(arr, function (x, i, a) { 69 | memo = iterator(memo, x, i, a); 70 | }); 71 | return memo; 72 | }; 73 | 74 | var _keys = function (obj) { 75 | if (Object.keys) { 76 | return Object.keys(obj); 77 | } 78 | var keys = []; 79 | for (var k in obj) { 80 | if (obj.hasOwnProperty(k)) { 81 | keys.push(k); 82 | } 83 | } 84 | return keys; 85 | }; 86 | 87 | //// exported async module functions //// 88 | 89 | //// nextTick implementation with browser-compatible fallback //// 90 | if (typeof process === 'undefined' || !(process.nextTick)) { 91 | if (typeof setImmediate === 'function') { 92 | async.nextTick = function (fn) { 93 | // not a direct alias for IE10 compatibility 94 | setImmediate(fn); 95 | }; 96 | async.setImmediate = async.nextTick; 97 | } 98 | else { 99 | async.nextTick = function (fn) { 100 | setTimeout(fn, 0); 101 | }; 102 | async.setImmediate = async.nextTick; 103 | } 104 | } 105 | else { 106 | async.nextTick = process.nextTick; 107 | if (typeof setImmediate !== 'undefined') { 108 | async.setImmediate = function (fn) { 109 | // not a direct alias for IE10 compatibility 110 | setImmediate(fn); 111 | }; 112 | } 113 | else { 114 | async.setImmediate = async.nextTick; 115 | } 116 | } 117 | 118 | async.each = function (arr, iterator, callback) { 119 | callback = callback || function () {}; 120 | if (!arr.length) { 121 | return callback(); 122 | } 123 | var completed = 0; 124 | _each(arr, function (x) { 125 | iterator(x, only_once(done) ); 126 | }); 127 | function done(err) { 128 | if (err) { 129 | callback(err); 130 | callback = function () {}; 131 | } 132 | else { 133 | completed += 1; 134 | if (completed >= arr.length) { 135 | callback(); 136 | } 137 | } 138 | } 139 | }; 140 | async.forEach = async.each; 141 | 142 | async.eachSeries = function (arr, iterator, callback) { 143 | callback = callback || function () {}; 144 | if (!arr.length) { 145 | return callback(); 146 | } 147 | var completed = 0; 148 | var iterate = function () { 149 | iterator(arr[completed], function (err) { 150 | if (err) { 151 | callback(err); 152 | callback = function () {}; 153 | } 154 | else { 155 | completed += 1; 156 | if (completed >= arr.length) { 157 | callback(); 158 | } 159 | else { 160 | iterate(); 161 | } 162 | } 163 | }); 164 | }; 165 | iterate(); 166 | }; 167 | async.forEachSeries = async.eachSeries; 168 | 169 | async.eachLimit = function (arr, limit, iterator, callback) { 170 | var fn = _eachLimit(limit); 171 | fn.apply(null, [arr, iterator, callback]); 172 | }; 173 | async.forEachLimit = async.eachLimit; 174 | 175 | var _eachLimit = function (limit) { 176 | 177 | return function (arr, iterator, callback) { 178 | callback = callback || function () {}; 179 | if (!arr.length || limit <= 0) { 180 | return callback(); 181 | } 182 | var completed = 0; 183 | var started = 0; 184 | var running = 0; 185 | 186 | (function replenish () { 187 | if (completed >= arr.length) { 188 | return callback(); 189 | } 190 | 191 | while (running < limit && started < arr.length) { 192 | started += 1; 193 | running += 1; 194 | iterator(arr[started - 1], function (err) { 195 | if (err) { 196 | callback(err); 197 | callback = function () {}; 198 | } 199 | else { 200 | completed += 1; 201 | running -= 1; 202 | if (completed >= arr.length) { 203 | callback(); 204 | } 205 | else { 206 | replenish(); 207 | } 208 | } 209 | }); 210 | } 211 | })(); 212 | }; 213 | }; 214 | 215 | 216 | var doParallel = function (fn) { 217 | return function () { 218 | var args = Array.prototype.slice.call(arguments); 219 | return fn.apply(null, [async.each].concat(args)); 220 | }; 221 | }; 222 | var doParallelLimit = function(limit, fn) { 223 | return function () { 224 | var args = Array.prototype.slice.call(arguments); 225 | return fn.apply(null, [_eachLimit(limit)].concat(args)); 226 | }; 227 | }; 228 | var doSeries = function (fn) { 229 | return function () { 230 | var args = Array.prototype.slice.call(arguments); 231 | return fn.apply(null, [async.eachSeries].concat(args)); 232 | }; 233 | }; 234 | 235 | 236 | var _asyncMap = function (eachfn, arr, iterator, callback) { 237 | arr = _map(arr, function (x, i) { 238 | return {index: i, value: x}; 239 | }); 240 | if (!callback) { 241 | eachfn(arr, function (x, callback) { 242 | iterator(x.value, function (err) { 243 | callback(err); 244 | }); 245 | }); 246 | } else { 247 | var results = []; 248 | eachfn(arr, function (x, callback) { 249 | iterator(x.value, function (err, v) { 250 | results[x.index] = v; 251 | callback(err); 252 | }); 253 | }, function (err) { 254 | callback(err, results); 255 | }); 256 | } 257 | }; 258 | async.map = doParallel(_asyncMap); 259 | async.mapSeries = doSeries(_asyncMap); 260 | async.mapLimit = function (arr, limit, iterator, callback) { 261 | return _mapLimit(limit)(arr, iterator, callback); 262 | }; 263 | 264 | var _mapLimit = function(limit) { 265 | return doParallelLimit(limit, _asyncMap); 266 | }; 267 | 268 | // reduce only has a series version, as doing reduce in parallel won't 269 | // work in many situations. 270 | async.reduce = function (arr, memo, iterator, callback) { 271 | async.eachSeries(arr, function (x, callback) { 272 | iterator(memo, x, function (err, v) { 273 | memo = v; 274 | callback(err); 275 | }); 276 | }, function (err) { 277 | callback(err, memo); 278 | }); 279 | }; 280 | // inject alias 281 | async.inject = async.reduce; 282 | // foldl alias 283 | async.foldl = async.reduce; 284 | 285 | async.reduceRight = function (arr, memo, iterator, callback) { 286 | var reversed = _map(arr, function (x) { 287 | return x; 288 | }).reverse(); 289 | async.reduce(reversed, memo, iterator, callback); 290 | }; 291 | // foldr alias 292 | async.foldr = async.reduceRight; 293 | 294 | var _filter = function (eachfn, arr, iterator, callback) { 295 | var results = []; 296 | arr = _map(arr, function (x, i) { 297 | return {index: i, value: x}; 298 | }); 299 | eachfn(arr, function (x, callback) { 300 | iterator(x.value, function (v) { 301 | if (v) { 302 | results.push(x); 303 | } 304 | callback(); 305 | }); 306 | }, function (err) { 307 | callback(_map(results.sort(function (a, b) { 308 | return a.index - b.index; 309 | }), function (x) { 310 | return x.value; 311 | })); 312 | }); 313 | }; 314 | async.filter = doParallel(_filter); 315 | async.filterSeries = doSeries(_filter); 316 | // select alias 317 | async.select = async.filter; 318 | async.selectSeries = async.filterSeries; 319 | 320 | var _reject = function (eachfn, arr, iterator, callback) { 321 | var results = []; 322 | arr = _map(arr, function (x, i) { 323 | return {index: i, value: x}; 324 | }); 325 | eachfn(arr, function (x, callback) { 326 | iterator(x.value, function (v) { 327 | if (!v) { 328 | results.push(x); 329 | } 330 | callback(); 331 | }); 332 | }, function (err) { 333 | callback(_map(results.sort(function (a, b) { 334 | return a.index - b.index; 335 | }), function (x) { 336 | return x.value; 337 | })); 338 | }); 339 | }; 340 | async.reject = doParallel(_reject); 341 | async.rejectSeries = doSeries(_reject); 342 | 343 | var _detect = function (eachfn, arr, iterator, main_callback) { 344 | eachfn(arr, function (x, callback) { 345 | iterator(x, function (result) { 346 | if (result) { 347 | main_callback(x); 348 | main_callback = function () {}; 349 | } 350 | else { 351 | callback(); 352 | } 353 | }); 354 | }, function (err) { 355 | main_callback(); 356 | }); 357 | }; 358 | async.detect = doParallel(_detect); 359 | async.detectSeries = doSeries(_detect); 360 | 361 | async.some = function (arr, iterator, main_callback) { 362 | async.each(arr, function (x, callback) { 363 | iterator(x, function (v) { 364 | if (v) { 365 | main_callback(true); 366 | main_callback = function () {}; 367 | } 368 | callback(); 369 | }); 370 | }, function (err) { 371 | main_callback(false); 372 | }); 373 | }; 374 | // any alias 375 | async.any = async.some; 376 | 377 | async.every = function (arr, iterator, main_callback) { 378 | async.each(arr, function (x, callback) { 379 | iterator(x, function (v) { 380 | if (!v) { 381 | main_callback(false); 382 | main_callback = function () {}; 383 | } 384 | callback(); 385 | }); 386 | }, function (err) { 387 | main_callback(true); 388 | }); 389 | }; 390 | // all alias 391 | async.all = async.every; 392 | 393 | async.sortBy = function (arr, iterator, callback) { 394 | async.map(arr, function (x, callback) { 395 | iterator(x, function (err, criteria) { 396 | if (err) { 397 | callback(err); 398 | } 399 | else { 400 | callback(null, {value: x, criteria: criteria}); 401 | } 402 | }); 403 | }, function (err, results) { 404 | if (err) { 405 | return callback(err); 406 | } 407 | else { 408 | var fn = function (left, right) { 409 | var a = left.criteria, b = right.criteria; 410 | return a < b ? -1 : a > b ? 1 : 0; 411 | }; 412 | callback(null, _map(results.sort(fn), function (x) { 413 | return x.value; 414 | })); 415 | } 416 | }); 417 | }; 418 | 419 | async.auto = function (tasks, callback) { 420 | callback = callback || function () {}; 421 | var keys = _keys(tasks); 422 | var remainingTasks = keys.length 423 | if (!remainingTasks) { 424 | return callback(); 425 | } 426 | 427 | var results = {}; 428 | 429 | var listeners = []; 430 | var addListener = function (fn) { 431 | listeners.unshift(fn); 432 | }; 433 | var removeListener = function (fn) { 434 | for (var i = 0; i < listeners.length; i += 1) { 435 | if (listeners[i] === fn) { 436 | listeners.splice(i, 1); 437 | return; 438 | } 439 | } 440 | }; 441 | var taskComplete = function () { 442 | remainingTasks-- 443 | _each(listeners.slice(0), function (fn) { 444 | fn(); 445 | }); 446 | }; 447 | 448 | addListener(function () { 449 | if (!remainingTasks) { 450 | var theCallback = callback; 451 | // prevent final callback from calling itself if it errors 452 | callback = function () {}; 453 | 454 | theCallback(null, results); 455 | } 456 | }); 457 | 458 | _each(keys, function (k) { 459 | var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; 460 | var taskCallback = function (err) { 461 | var args = Array.prototype.slice.call(arguments, 1); 462 | if (args.length <= 1) { 463 | args = args[0]; 464 | } 465 | if (err) { 466 | var safeResults = {}; 467 | _each(_keys(results), function(rkey) { 468 | safeResults[rkey] = results[rkey]; 469 | }); 470 | safeResults[k] = args; 471 | callback(err, safeResults); 472 | // stop subsequent errors hitting callback multiple times 473 | callback = function () {}; 474 | } 475 | else { 476 | results[k] = args; 477 | async.setImmediate(taskComplete); 478 | } 479 | }; 480 | var requires = task.slice(0, Math.abs(task.length - 1)) || []; 481 | var ready = function () { 482 | return _reduce(requires, function (a, x) { 483 | return (a && results.hasOwnProperty(x)); 484 | }, true) && !results.hasOwnProperty(k); 485 | }; 486 | if (ready()) { 487 | task[task.length - 1](taskCallback, results); 488 | } 489 | else { 490 | var listener = function () { 491 | if (ready()) { 492 | removeListener(listener); 493 | task[task.length - 1](taskCallback, results); 494 | } 495 | }; 496 | addListener(listener); 497 | } 498 | }); 499 | }; 500 | 501 | async.retry = function(times, task, callback) { 502 | var DEFAULT_TIMES = 5; 503 | var attempts = []; 504 | // Use defaults if times not passed 505 | if (typeof times === 'function') { 506 | callback = task; 507 | task = times; 508 | times = DEFAULT_TIMES; 509 | } 510 | // Make sure times is a number 511 | times = parseInt(times, 10) || DEFAULT_TIMES; 512 | var wrappedTask = function(wrappedCallback, wrappedResults) { 513 | var retryAttempt = function(task, finalAttempt) { 514 | return function(seriesCallback) { 515 | task(function(err, result){ 516 | seriesCallback(!err || finalAttempt, {err: err, result: result}); 517 | }, wrappedResults); 518 | }; 519 | }; 520 | while (times) { 521 | attempts.push(retryAttempt(task, !(times-=1))); 522 | } 523 | async.series(attempts, function(done, data){ 524 | data = data[data.length - 1]; 525 | (wrappedCallback || callback)(data.err, data.result); 526 | }); 527 | } 528 | // If a callback is passed, run this as a controll flow 529 | return callback ? wrappedTask() : wrappedTask 530 | }; 531 | 532 | async.waterfall = function (tasks, callback) { 533 | callback = callback || function () {}; 534 | if (!_isArray(tasks)) { 535 | var err = new Error('First argument to waterfall must be an array of functions'); 536 | return callback(err); 537 | } 538 | if (!tasks.length) { 539 | return callback(); 540 | } 541 | var wrapIterator = function (iterator) { 542 | return function (err) { 543 | if (err) { 544 | callback.apply(null, arguments); 545 | callback = function () {}; 546 | } 547 | else { 548 | var args = Array.prototype.slice.call(arguments, 1); 549 | var next = iterator.next(); 550 | if (next) { 551 | args.push(wrapIterator(next)); 552 | } 553 | else { 554 | args.push(callback); 555 | } 556 | async.setImmediate(function () { 557 | iterator.apply(null, args); 558 | }); 559 | } 560 | }; 561 | }; 562 | wrapIterator(async.iterator(tasks))(); 563 | }; 564 | 565 | var _parallel = function(eachfn, tasks, callback) { 566 | callback = callback || function () {}; 567 | if (_isArray(tasks)) { 568 | eachfn.map(tasks, function (fn, callback) { 569 | if (fn) { 570 | fn(function (err) { 571 | var args = Array.prototype.slice.call(arguments, 1); 572 | if (args.length <= 1) { 573 | args = args[0]; 574 | } 575 | callback.call(null, err, args); 576 | }); 577 | } 578 | }, callback); 579 | } 580 | else { 581 | var results = {}; 582 | eachfn.each(_keys(tasks), function (k, callback) { 583 | tasks[k](function (err) { 584 | var args = Array.prototype.slice.call(arguments, 1); 585 | if (args.length <= 1) { 586 | args = args[0]; 587 | } 588 | results[k] = args; 589 | callback(err); 590 | }); 591 | }, function (err) { 592 | callback(err, results); 593 | }); 594 | } 595 | }; 596 | 597 | async.parallel = function (tasks, callback) { 598 | _parallel({ map: async.map, each: async.each }, tasks, callback); 599 | }; 600 | 601 | async.parallelLimit = function(tasks, limit, callback) { 602 | _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); 603 | }; 604 | 605 | async.series = function (tasks, callback) { 606 | callback = callback || function () {}; 607 | if (_isArray(tasks)) { 608 | async.mapSeries(tasks, function (fn, callback) { 609 | if (fn) { 610 | fn(function (err) { 611 | var args = Array.prototype.slice.call(arguments, 1); 612 | if (args.length <= 1) { 613 | args = args[0]; 614 | } 615 | callback.call(null, err, args); 616 | }); 617 | } 618 | }, callback); 619 | } 620 | else { 621 | var results = {}; 622 | async.eachSeries(_keys(tasks), function (k, callback) { 623 | tasks[k](function (err) { 624 | var args = Array.prototype.slice.call(arguments, 1); 625 | if (args.length <= 1) { 626 | args = args[0]; 627 | } 628 | results[k] = args; 629 | callback(err); 630 | }); 631 | }, function (err) { 632 | callback(err, results); 633 | }); 634 | } 635 | }; 636 | 637 | async.iterator = function (tasks) { 638 | var makeCallback = function (index) { 639 | var fn = function () { 640 | if (tasks.length) { 641 | tasks[index].apply(null, arguments); 642 | } 643 | return fn.next(); 644 | }; 645 | fn.next = function () { 646 | return (index < tasks.length - 1) ? makeCallback(index + 1): null; 647 | }; 648 | return fn; 649 | }; 650 | return makeCallback(0); 651 | }; 652 | 653 | async.apply = function (fn) { 654 | var args = Array.prototype.slice.call(arguments, 1); 655 | return function () { 656 | return fn.apply( 657 | null, args.concat(Array.prototype.slice.call(arguments)) 658 | ); 659 | }; 660 | }; 661 | 662 | var _concat = function (eachfn, arr, fn, callback) { 663 | var r = []; 664 | eachfn(arr, function (x, cb) { 665 | fn(x, function (err, y) { 666 | r = r.concat(y || []); 667 | cb(err); 668 | }); 669 | }, function (err) { 670 | callback(err, r); 671 | }); 672 | }; 673 | async.concat = doParallel(_concat); 674 | async.concatSeries = doSeries(_concat); 675 | 676 | async.whilst = function (test, iterator, callback) { 677 | if (test()) { 678 | iterator(function (err) { 679 | if (err) { 680 | return callback(err); 681 | } 682 | async.whilst(test, iterator, callback); 683 | }); 684 | } 685 | else { 686 | callback(); 687 | } 688 | }; 689 | 690 | async.doWhilst = function (iterator, test, callback) { 691 | iterator(function (err) { 692 | if (err) { 693 | return callback(err); 694 | } 695 | var args = Array.prototype.slice.call(arguments, 1); 696 | if (test.apply(null, args)) { 697 | async.doWhilst(iterator, test, callback); 698 | } 699 | else { 700 | callback(); 701 | } 702 | }); 703 | }; 704 | 705 | async.until = function (test, iterator, callback) { 706 | if (!test()) { 707 | iterator(function (err) { 708 | if (err) { 709 | return callback(err); 710 | } 711 | async.until(test, iterator, callback); 712 | }); 713 | } 714 | else { 715 | callback(); 716 | } 717 | }; 718 | 719 | async.doUntil = function (iterator, test, callback) { 720 | iterator(function (err) { 721 | if (err) { 722 | return callback(err); 723 | } 724 | var args = Array.prototype.slice.call(arguments, 1); 725 | if (!test.apply(null, args)) { 726 | async.doUntil(iterator, test, callback); 727 | } 728 | else { 729 | callback(); 730 | } 731 | }); 732 | }; 733 | 734 | async.queue = function (worker, concurrency) { 735 | if (concurrency === undefined) { 736 | concurrency = 1; 737 | } 738 | function _insert(q, data, pos, callback) { 739 | if (!q.started){ 740 | q.started = true; 741 | } 742 | if (!_isArray(data)) { 743 | data = [data]; 744 | } 745 | if(data.length == 0) { 746 | // call drain immediately if there are no tasks 747 | return async.setImmediate(function() { 748 | if (q.drain) { 749 | q.drain(); 750 | } 751 | }); 752 | } 753 | _each(data, function(task) { 754 | var item = { 755 | data: task, 756 | callback: typeof callback === 'function' ? callback : null 757 | }; 758 | 759 | if (pos) { 760 | q.tasks.unshift(item); 761 | } else { 762 | q.tasks.push(item); 763 | } 764 | 765 | if (q.saturated && q.tasks.length === q.concurrency) { 766 | q.saturated(); 767 | } 768 | async.setImmediate(q.process); 769 | }); 770 | } 771 | 772 | var workers = 0; 773 | var q = { 774 | tasks: [], 775 | concurrency: concurrency, 776 | saturated: null, 777 | empty: null, 778 | drain: null, 779 | started: false, 780 | paused: false, 781 | push: function (data, callback) { 782 | _insert(q, data, false, callback); 783 | }, 784 | kill: function () { 785 | q.drain = null; 786 | q.tasks = []; 787 | }, 788 | unshift: function (data, callback) { 789 | _insert(q, data, true, callback); 790 | }, 791 | process: function () { 792 | if (!q.paused && workers < q.concurrency && q.tasks.length) { 793 | var task = q.tasks.shift(); 794 | if (q.empty && q.tasks.length === 0) { 795 | q.empty(); 796 | } 797 | workers += 1; 798 | var next = function () { 799 | workers -= 1; 800 | if (task.callback) { 801 | task.callback.apply(task, arguments); 802 | } 803 | if (q.drain && q.tasks.length + workers === 0) { 804 | q.drain(); 805 | } 806 | q.process(); 807 | }; 808 | var cb = only_once(next); 809 | worker(task.data, cb); 810 | } 811 | }, 812 | length: function () { 813 | return q.tasks.length; 814 | }, 815 | running: function () { 816 | return workers; 817 | }, 818 | idle: function() { 819 | return q.tasks.length + workers === 0; 820 | }, 821 | pause: function () { 822 | if (q.paused === true) { return; } 823 | q.paused = true; 824 | }, 825 | resume: function () { 826 | if (q.paused === false) { return; } 827 | q.paused = false; 828 | // Need to call q.process once per concurrent 829 | // worker to preserve full concurrency after pause 830 | for (var w = 1; w <= q.concurrency; w++) { 831 | async.setImmediate(q.process); 832 | } 833 | } 834 | }; 835 | return q; 836 | }; 837 | 838 | async.priorityQueue = function (worker, concurrency) { 839 | 840 | function _compareTasks(a, b){ 841 | return a.priority - b.priority; 842 | }; 843 | 844 | function _binarySearch(sequence, item, compare) { 845 | var beg = -1, 846 | end = sequence.length - 1; 847 | while (beg < end) { 848 | var mid = beg + ((end - beg + 1) >>> 1); 849 | if (compare(item, sequence[mid]) >= 0) { 850 | beg = mid; 851 | } else { 852 | end = mid - 1; 853 | } 854 | } 855 | return beg; 856 | } 857 | 858 | function _insert(q, data, priority, callback) { 859 | if (!q.started){ 860 | q.started = true; 861 | } 862 | if (!_isArray(data)) { 863 | data = [data]; 864 | } 865 | if(data.length == 0) { 866 | // call drain immediately if there are no tasks 867 | return async.setImmediate(function() { 868 | if (q.drain) { 869 | q.drain(); 870 | } 871 | }); 872 | } 873 | _each(data, function(task) { 874 | var item = { 875 | data: task, 876 | priority: priority, 877 | callback: typeof callback === 'function' ? callback : null 878 | }; 879 | 880 | q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); 881 | 882 | if (q.saturated && q.tasks.length === q.concurrency) { 883 | q.saturated(); 884 | } 885 | async.setImmediate(q.process); 886 | }); 887 | } 888 | 889 | // Start with a normal queue 890 | var q = async.queue(worker, concurrency); 891 | 892 | // Override push to accept second parameter representing priority 893 | q.push = function (data, priority, callback) { 894 | _insert(q, data, priority, callback); 895 | }; 896 | 897 | // Remove unshift function 898 | delete q.unshift; 899 | 900 | return q; 901 | }; 902 | 903 | async.cargo = function (worker, payload) { 904 | var working = false, 905 | tasks = []; 906 | 907 | var cargo = { 908 | tasks: tasks, 909 | payload: payload, 910 | saturated: null, 911 | empty: null, 912 | drain: null, 913 | drained: true, 914 | push: function (data, callback) { 915 | if (!_isArray(data)) { 916 | data = [data]; 917 | } 918 | _each(data, function(task) { 919 | tasks.push({ 920 | data: task, 921 | callback: typeof callback === 'function' ? callback : null 922 | }); 923 | cargo.drained = false; 924 | if (cargo.saturated && tasks.length === payload) { 925 | cargo.saturated(); 926 | } 927 | }); 928 | async.setImmediate(cargo.process); 929 | }, 930 | process: function process() { 931 | if (working) return; 932 | if (tasks.length === 0) { 933 | if(cargo.drain && !cargo.drained) cargo.drain(); 934 | cargo.drained = true; 935 | return; 936 | } 937 | 938 | var ts = typeof payload === 'number' 939 | ? tasks.splice(0, payload) 940 | : tasks.splice(0, tasks.length); 941 | 942 | var ds = _map(ts, function (task) { 943 | return task.data; 944 | }); 945 | 946 | if(cargo.empty) cargo.empty(); 947 | working = true; 948 | worker(ds, function () { 949 | working = false; 950 | 951 | var args = arguments; 952 | _each(ts, function (data) { 953 | if (data.callback) { 954 | data.callback.apply(null, args); 955 | } 956 | }); 957 | 958 | process(); 959 | }); 960 | }, 961 | length: function () { 962 | return tasks.length; 963 | }, 964 | running: function () { 965 | return working; 966 | } 967 | }; 968 | return cargo; 969 | }; 970 | 971 | var _console_fn = function (name) { 972 | return function (fn) { 973 | var args = Array.prototype.slice.call(arguments, 1); 974 | fn.apply(null, args.concat([function (err) { 975 | var args = Array.prototype.slice.call(arguments, 1); 976 | if (typeof console !== 'undefined') { 977 | if (err) { 978 | if (console.error) { 979 | console.error(err); 980 | } 981 | } 982 | else if (console[name]) { 983 | _each(args, function (x) { 984 | console[name](x); 985 | }); 986 | } 987 | } 988 | }])); 989 | }; 990 | }; 991 | async.log = _console_fn('log'); 992 | async.dir = _console_fn('dir'); 993 | /*async.info = _console_fn('info'); 994 | async.warn = _console_fn('warn'); 995 | async.error = _console_fn('error');*/ 996 | 997 | async.memoize = function (fn, hasher) { 998 | var memo = {}; 999 | var queues = {}; 1000 | hasher = hasher || function (x) { 1001 | return x; 1002 | }; 1003 | var memoized = function () { 1004 | var args = Array.prototype.slice.call(arguments); 1005 | var callback = args.pop(); 1006 | var key = hasher.apply(null, args); 1007 | if (key in memo) { 1008 | async.nextTick(function () { 1009 | callback.apply(null, memo[key]); 1010 | }); 1011 | } 1012 | else if (key in queues) { 1013 | queues[key].push(callback); 1014 | } 1015 | else { 1016 | queues[key] = [callback]; 1017 | fn.apply(null, args.concat([function () { 1018 | memo[key] = arguments; 1019 | var q = queues[key]; 1020 | delete queues[key]; 1021 | for (var i = 0, l = q.length; i < l; i++) { 1022 | q[i].apply(null, arguments); 1023 | } 1024 | }])); 1025 | } 1026 | }; 1027 | memoized.memo = memo; 1028 | memoized.unmemoized = fn; 1029 | return memoized; 1030 | }; 1031 | 1032 | async.unmemoize = function (fn) { 1033 | return function () { 1034 | return (fn.unmemoized || fn).apply(null, arguments); 1035 | }; 1036 | }; 1037 | 1038 | async.times = function (count, iterator, callback) { 1039 | var counter = []; 1040 | for (var i = 0; i < count; i++) { 1041 | counter.push(i); 1042 | } 1043 | return async.map(counter, iterator, callback); 1044 | }; 1045 | 1046 | async.timesSeries = function (count, iterator, callback) { 1047 | var counter = []; 1048 | for (var i = 0; i < count; i++) { 1049 | counter.push(i); 1050 | } 1051 | return async.mapSeries(counter, iterator, callback); 1052 | }; 1053 | 1054 | async.seq = function (/* functions... */) { 1055 | var fns = arguments; 1056 | return function () { 1057 | var that = this; 1058 | var args = Array.prototype.slice.call(arguments); 1059 | var callback = args.pop(); 1060 | async.reduce(fns, args, function (newargs, fn, cb) { 1061 | fn.apply(that, newargs.concat([function () { 1062 | var err = arguments[0]; 1063 | var nextargs = Array.prototype.slice.call(arguments, 1); 1064 | cb(err, nextargs); 1065 | }])) 1066 | }, 1067 | function (err, results) { 1068 | callback.apply(that, [err].concat(results)); 1069 | }); 1070 | }; 1071 | }; 1072 | 1073 | async.compose = function (/* functions... */) { 1074 | return async.seq.apply(null, Array.prototype.reverse.call(arguments)); 1075 | }; 1076 | 1077 | var _applyEach = function (eachfn, fns /*args...*/) { 1078 | var go = function () { 1079 | var that = this; 1080 | var args = Array.prototype.slice.call(arguments); 1081 | var callback = args.pop(); 1082 | return eachfn(fns, function (fn, cb) { 1083 | fn.apply(that, args.concat([cb])); 1084 | }, 1085 | callback); 1086 | }; 1087 | if (arguments.length > 2) { 1088 | var args = Array.prototype.slice.call(arguments, 2); 1089 | return go.apply(this, args); 1090 | } 1091 | else { 1092 | return go; 1093 | } 1094 | }; 1095 | async.applyEach = doParallel(_applyEach); 1096 | async.applyEachSeries = doSeries(_applyEach); 1097 | 1098 | async.forever = function (fn, callback) { 1099 | function next(err) { 1100 | if (err) { 1101 | if (callback) { 1102 | return callback(err); 1103 | } 1104 | throw err; 1105 | } 1106 | fn(next); 1107 | } 1108 | next(); 1109 | }; 1110 | 1111 | // Node.js 1112 | if (typeof module !== 'undefined' && module.exports) { 1113 | module.exports = async; 1114 | } 1115 | // AMD / RequireJS 1116 | else if (typeof define !== 'undefined' && define.amd) { 1117 | define([], function () { 1118 | return async; 1119 | }); 1120 | } 1121 | // included directly via