Repository: docker-archive-public/docker.kitematic Branch: master Commit: 445bfbae698c Files: 194 Total size: 561.1 KB Directory structure: gitextract_yfo7g9s8/ ├── .babelrc ├── .circleci/ │ └── config.yml ├── .eslintrc ├── .github/ │ └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── ROADMAP.md ├── __integration__/ │ ├── HubUtil-integration.js │ └── RegHubUtil-integration.js ├── __mocks__/ │ ├── app.js │ ├── electron.js │ └── remote.js ├── __tests__/ │ └── Util-test.js ├── docs/ │ └── README.md ├── electron-builder.json ├── index.html ├── jest-integration.json ├── jest-unit.json ├── package.json ├── resources/ │ ├── MSYS_LICENSE │ ├── OPENSSH_LICENSE │ └── terminal ├── src/ │ ├── actions/ │ │ ├── AccountActions.js │ │ ├── AccountServerActions.js │ │ ├── ContainerActions.js │ │ ├── ContainerServerActions.js │ │ ├── ImageActions.js │ │ ├── ImageServerActions.js │ │ ├── NetworkActions.js │ │ ├── RepositoryActions.js │ │ ├── RepositoryServerActions.js │ │ ├── SetupActions.js │ │ ├── SetupServerActions.js │ │ ├── TagActions.js │ │ └── TagServerActions.js │ ├── alt.js │ ├── app.js │ ├── browser.js │ ├── components/ │ │ ├── About.react.js │ │ ├── Account.react.js │ │ ├── AccountLogin.react.js │ │ ├── AccountSignup.react.js │ │ ├── ContainerDetails.react.js │ │ ├── ContainerDetailsHeader.react.js │ │ ├── ContainerDetailsSubheader.react.js │ │ ├── ContainerHome.react.js │ │ ├── ContainerHomeFolders.react.js │ │ ├── ContainerHomeIpPortsPreview.react.js │ │ ├── ContainerHomeLogs.react.js │ │ ├── ContainerList.react.js │ │ ├── ContainerListItem.react.js │ │ ├── ContainerProgress.react.js │ │ ├── ContainerSettings.react.js │ │ ├── ContainerSettingsAdvanced.react.js │ │ ├── ContainerSettingsGeneral.react.js │ │ ├── ContainerSettingsNetwork.react.js │ │ ├── ContainerSettingsPorts.react.js │ │ ├── ContainerSettingsVolumes.react.js │ │ ├── Containers.react.js │ │ ├── Header.react.js │ │ ├── ImageCard.react.js │ │ ├── Loading.react.js │ │ ├── NewContainerSearch.react.js │ │ ├── Preferences.react.js │ │ ├── Radial.react.js │ │ └── Setup.react.js │ ├── main.js │ ├── main.ts │ ├── menutemplate.js │ ├── router.js │ ├── routes.js │ ├── stores/ │ │ ├── AccountStore.js │ │ ├── ContainerStore.js │ │ ├── ImageStore.js │ │ ├── NetworkStore.js │ │ ├── RepositoryStore.js │ │ ├── SetupStore.js │ │ └── TagStore.js │ └── utils/ │ ├── ContainerUtil.js │ ├── DockerMachineUtil.js │ ├── DockerUtil.js │ ├── HubUtil.js │ ├── MetricsUtil.js │ ├── RegHubUtil.js │ ├── SetupUtil.js │ ├── Util.js │ ├── VirtualBoxUtil.js │ └── WebUtil.js ├── styles/ │ ├── animation.less │ ├── bootstrap/ │ │ ├── alerts.less │ │ ├── badges.less │ │ ├── bootstrap.less │ │ ├── breadcrumbs.less │ │ ├── button-groups.less │ │ ├── buttons.less │ │ ├── carousel.less │ │ ├── close.less │ │ ├── code.less │ │ ├── component-animations.less │ │ ├── dropdowns.less │ │ ├── forms.less │ │ ├── glyphicons.less │ │ ├── grid.less │ │ ├── input-groups.less │ │ ├── jumbotron.less │ │ ├── labels.less │ │ ├── list-group.less │ │ ├── media.less │ │ ├── mixins/ │ │ │ ├── alerts.less │ │ │ ├── background-variant.less │ │ │ ├── border-radius.less │ │ │ ├── buttons.less │ │ │ ├── center-block.less │ │ │ ├── clearfix.less │ │ │ ├── forms.less │ │ │ ├── gradients.less │ │ │ ├── grid-framework.less │ │ │ ├── grid.less │ │ │ ├── hide-text.less │ │ │ ├── image.less │ │ │ ├── labels.less │ │ │ ├── list-group.less │ │ │ ├── nav-divider.less │ │ │ ├── nav-vertical-align.less │ │ │ ├── opacity.less │ │ │ ├── pagination.less │ │ │ ├── panels.less │ │ │ ├── progress-bar.less │ │ │ ├── reset-filter.less │ │ │ ├── resize.less │ │ │ ├── responsive-visibility.less │ │ │ ├── size.less │ │ │ ├── tab-focus.less │ │ │ ├── table-row.less │ │ │ ├── text-emphasis.less │ │ │ ├── text-overflow.less │ │ │ └── vendor-prefixes.less │ │ ├── mixins.less │ │ ├── modals.less │ │ ├── navbar.less │ │ ├── navs.less │ │ ├── normalize.less │ │ ├── pager.less │ │ ├── pagination.less │ │ ├── panels.less │ │ ├── popovers.less │ │ ├── print.less │ │ ├── progress-bars.less │ │ ├── responsive-embed.less │ │ ├── responsive-utilities.less │ │ ├── scaffolding.less │ │ ├── tables.less │ │ ├── theme.less │ │ ├── thumbnails.less │ │ ├── tooltip.less │ │ ├── type.less │ │ ├── utilities.less │ │ ├── variables.less │ │ └── wells.less │ ├── container-home.less │ ├── container-logs.less │ ├── container-progress.less │ ├── container-settings.less │ ├── header.less │ ├── icons.less │ ├── layout.less │ ├── left-panel.less │ ├── loading.less │ ├── main.less │ ├── mixins.less │ ├── new-container.less │ ├── preferences.less │ ├── radial.less │ ├── retina.less │ ├── right-panel.less │ ├── setup.less │ ├── spinner.less │ ├── theme.less │ └── variables.less ├── tsconfig.json ├── tslint.json └── util/ ├── Info.plist ├── VirtualBox_Uninstall.tool ├── kitematic.icns ├── prepare.js ├── reset ├── reset.ps1 └── testenv.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "env", "react" ], "plugins": [ "transform-runtime", "transform-async-to-generator" ] } ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: test: macos: xcode: "9.0" steps: - run: name: Install node@10 command: | set +e touch $BASH_ENV curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV echo 'nvm install 10' >> $BASH_ENV echo 'nvm alias default 10' >> $BASH_ENV - run: name: Install wine command: brew install wine - checkout - run: npm install - run: npm test - run: npm run release:mac - run: npm run release:windows workflows: version: 2 test: jobs: - test ================================================ FILE: .eslintrc ================================================ root: true plugins: - react parserOptions: ecmaVersion: 2017 sourceType: module env: node: true es6: true browser: true jest: true extends: "eslint:recommended" rules: indent: [2, 2, {SwitchCase: 1}] brace-style: [2, "1tbs"] camelcase: [2, { properties: "never" }] callback-return: [2, ["cb", "callback", "next"]] comma-spacing: 2 comma-style: [2, "last"] consistent-return: 2 curly: [2, "all"] default-case: 2 dot-notation: [2, { allowKeywords: true }] eol-last: 2 eqeqeq: 2 func-style: [2, "declaration"] guard-for-in: 2 key-spacing: [2, { beforeColon: false, afterColon: true }] new-cap: 2 new-parens: 2 no-alert: 2 no-array-constructor: 2 no-caller: 2 no-console: 0 no-delete-var: 2 no-empty-label: 2 no-eval: 2 no-extend-native: 2 no-extra-bind: 2 no-fallthrough: 2 no-floating-decimal: 2 no-implied-eval: 2 no-invalid-this: 2 no-iterator: 2 no-label-var: 2 no-labels: 2 no-lone-blocks: 2 no-loop-func: 2 no-mixed-spaces-and-tabs: [2, false] no-multi-spaces: 2 no-multi-str: 2 no-native-reassign: 2 no-nested-ternary: 2 no-new: 2 no-new-func: 2 no-new-object: 2 no-new-wrappers: 2 no-octal: 2 no-octal-escape: 2 no-process-exit: 2 no-proto: 2 no-redeclare: 2 no-return-assign: 2 no-script-url: 2 no-sequences: 2 no-shadow: 2 no-shadow-restricted-names: 2 no-spaced-func: 2 no-trailing-spaces: 2 no-undef: 2 no-undef-init: 2 no-undefined: 2 no-underscore-dangle: 2 no-unused-expressions: 2 no-unused-vars: [2, {vars: "all", args: "after-used"}] no-use-before-define: 2 no-with: 2 quotes: [2, "single"] radix: 2 semi: 2 semi-spacing: [2, {before: false, after: true}] space-after-keywords: [2, "always"] space-before-blocks: 2 space-before-function-paren: [2, "always"] space-infix-ops: 2 space-return-throw-case: 2 space-unary-ops: [2, {words: true, nonwords: false}] spaced-comment: [2, "always", { exceptions: ["-"]}] strict: [2, "global"] valid-jsdoc: [2, { prefer: { "return": "returns"}}] wrap-iife: 2 yoda: [2, "never"] # Previously on by default in node environment no-catch-shadow: 0 no-mixed-requires: 2 no-new-require: 2 no-path-concat: 2 global-strict: [0, "always"] handle-callback-err: [2, "err"] ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Expected behavior ### Actual behavior ### Information about the Issue ### Steps to reproduce the behavior 1. ... 2. ... ================================================ FILE: .gitignore ================================================ .DS_Store .swp build dist dist-electron-builder/ release src/**/*.js.map installer node_modules coverage npm-debug.log # Integration test environment integration # Resources resources/docker* resources/boot2docker* # Cache cache # Tests .test settings.json # IDEs .idea ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js node_js: - "8" - "10" cache: directories: - node_modules script: - npm install - npm test install: npm i ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Kitematic Thanks for contributing and supporting the Kitematic project! Before you file an issue or a pull request, read the following tips on how to keep development of the project awesome for all of the contributors: ## Table of Contents - [Mac Prerequisites](#prerequisites-for-developing-kitematic-on-mac) - [Windows Prerequisites](#prerequisites-for-developing-kitematic-on-windows) - [Getting Started](#getting-started) - [Architecture](#architecture) - [GitHub Issues](#github-issues) - [Pull Requests](#pull-requests) - [Code Guidelines](#code-guidelines) - [Testing](#testing) - [License](#license) ### Prerequisites for developing Kitematic on Mac You will need to install: - The [Docker Toolbox](https://docker.com/toolbox) - [Node.js](https://nodejs.org/) node version 10 is not supported - Wine `brew install wine` (only if you want to generate a Windows release on OS X) - The latest Xcode from the Apple App Store. ### Prerequisites for developing Kitematic on Windows You will need to install: - The [Docker Toolbox](https://docker.com/toolbox) - [Node.js](https://nodejs.org/) - Open a command prompt (`cmd`) and run the command `mkdir ~/AppData/Roaming/npm` - [Visual Studio 2013 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) (or similar) - You do not need to install any optional packages during install. - [Python](https://www.python.org/downloads/release/python-2710/) ![Toolbox Env Var](https://cloud.githubusercontent.com/assets/251292/10656552/adaedb20-7834-11e5-8881-d5402d3fee37.png) ### Getting Started - `npm install` To run the app in development: - `npm start` Running `npm start` will download and install the Docker client, [Docker Machine](https://github.com/docker/machine), [Docker Compose](https://github.com/docker/compose) the [Boot2Docker iso](https://github.com/boot2docker/boot2docker), [Electron](http://electron.atom.io/). ### Building & Release - `npm run release` ### Unit Tests - `npm test` ## Architecture ### Overview **Note: This architecture is work in progress and doesn't reflect the current state of the app, yet!** Kitematic is an application built using [electron](https://github.com/atom/electron) and is powered by the [Docker Engine](https://github.com/docker/docker). While it's work in progress, the goal is to make Kitematic a high-performance, portable Javascript ES6 application built with React and Flux (using [alt](https://github.com/goatslacker/alt). It adopts a single data flow pattern: ``` ╔═════════╗ ╔════════╗ ╔═════════════════╗ ║ Actions ║──────>║ Stores ║──────>║ View Components ║ ╚═════════╝ ╚════════╝ ╚═════════════════╝ ^ │ └──────────────────────────────────────┘ ``` There are three primary types of objects: - **Actions**: Interact with the system (Docker Engine, Docker Machine, Registries, Hub, etc) - **Views**: Views make up the UI, and trigger available actions. - **Stores**: Stores store the state of the application. and since Kitematic has a large amount of interaction with outside systems, we've added utils: - **Utils**: Utils interact with APIs, outside systems, CLI tools and generate. They are called by user-generated actions and in return, also create actions based on API return values, CLI output etc. ### Guidelines - Avoid asynchronous code in Actions, Stores or Views. Instead, put code involving callbacks, promises or generators in utils or actions. ## GitHub Issues Please try and label any issue as: - `bug`: clearly a defect or unwanted behavior (errors, performance issues) - `enhancement`: making an existing, working feature better (UI improvements, better integration) - `feature`: an entirely new feature. Please work on [roadmap features](https://github.com/kitematic/kitematic/blob/master/ROADMAP.md). Before creating an issue, please: 1. **Search the existing issues** to see if an issue already exists (and if so, throw in a handy :+1:)! 2. **Make sure you're running the latest version of Kitematic**. The bug may already be fixed! 3. **Explain how to reproduce the bug**. This will save maintainers tons of time! Please be as detailed as possible. Include a description of your environment and steps on how to reproduce a bug. ## Pull Requests We're thrilled to receive pull requests of any kind. Anything from bug fix, tests or new features are welcome. That said, please let us know what you're planning to do! For large changes always create a proposal. Maintainers will love to give you advice on building it and it keeps the app's design coherent. ### Pull Request Requirements: - Includes tests - [Signed Off](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work) ## Testing Please try to test any new code. - Tests can be run using `npm test` - Kitematic uses the [Jest framework](https://facebook.github.io/jest/) by Facebook. To keep tests fast, please mock as much as possible. ## Code Guidelines ### Javascript Kitematic is es6 ready. Please use es6 constructs where possible, they are powerful and make the code more succinct, understandable and fun. - Semicolons - 2 spaces (no tabs) #### Checking Javascript code standards with ESlint Run `npm run lint` before committing to ensure your javascript is up to standard. Feel free to suggest changes to the lint spec in `.eslintrc`. We designed Kitematic to be easy to build, extend and distribute for developers. ## License By contributing your code, you agree to license your contribution under the [Apache license](https://github.com/kitematic/kitematic/blob/master/LICENSE). ================================================ FILE: Gruntfile.js ================================================ var packagejson = require('./package.json'); var electron = require('electron'); module.exports = function (grunt) { require('load-grunt-tasks')(grunt); var target = grunt.option('target') || 'development'; var env = process.env; env.NODE_PATH = '..:' + env.NODE_PATH; env.NODE_ENV = target; var BASENAME = 'Kitematic'; var OSX_OUT = './dist'; var OSX_OUT_X64 = OSX_OUT + '/' + BASENAME + '-darwin-x64'; var OSX_FILENAME = OSX_OUT_X64 + '/' + BASENAME + '.app'; var LINUX_FILENAME = OSX_OUT + '/' + BASENAME + '_' + packagejson.version + '_amd64.deb'; var VERSION_FILENAME = BASENAME + '-' + packagejson.version; grunt.initConfig({ IDENTITY: 'Developer ID Application: Docker Inc', OSX_FILENAME, OSX_FILENAME_ESCAPED: OSX_FILENAME.replace(/ /g, '\\ ').replace(/\(/g, '\\(').replace(/\)/g, '\\)'), LINUX_FILENAME, VERSION_FILENAME, // electron electron: { windows: { options: { name: BASENAME, dir: 'build/', out: 'dist', version: packagejson['electron-version'], platform: 'win32', arch: 'x64', asar: true, icon: 'util/kitematic.ico', } }, osx: { options: { name: BASENAME, dir: 'build/', out: 'dist', version: packagejson['electron-version'], platform: 'darwin', arch: 'x64', asar: true, 'app-version': packagejson.version, icon: 'util/kitematic.icns', }, }, linux: { options: { name: BASENAME, dir: 'build/', out: 'dist', version: packagejson['electron-version'], platform: 'linux', arch: 'x64', asar: true, 'app-bundle-id': 'com.kitematic.kitematic', 'app-version': packagejson.version, icon: 'util/kitematic.png', } } }, rcedit: { exes: { files: [{ expand: true, cwd: 'dist/' + BASENAME + '-win32-x64', src: [BASENAME + '.exe'], }], options: { icon: 'util/kitematic.ico', 'file-version': packagejson.version, 'product-version': packagejson.version, 'version-string': { 'CompanyName': 'Docker', 'ProductVersion': packagejson.version, 'ProductName': BASENAME, 'FileDescription': BASENAME, 'InternalName': BASENAME + '.exe', 'OriginalFilename': BASENAME + '.exe', 'LegalCopyright': 'Copyright 2015-2016 Docker Inc. All rights reserved.' } } } }, // images copy: { dev: { files: [{ expand: true, cwd: '.', src: ['package.json', 'settings.json', 'index.html'], dest: 'build/' }, { expand: true, cwd: 'images/', src: ['**/*'], dest: 'build/' }, { src: 'util/kitematic.icns', dest: 'build/icon.icns', }, { src: 'util/kitematic.ico', dest: 'build/icon.ico', }, { src: 'util/kitematic.png', dest: 'build/icon.png', }, { expand: true, cwd: 'fonts/', src: ['**/*'], dest: 'build/' }, { cwd: 'node_modules/', src: Object.keys(packagejson.dependencies).map(function (dep) { return dep + '/**/*'; }), dest: 'build/node_modules/', expand: true }] }, windows: { files: [{ expand: true, cwd: 'resources', src: ['ssh.exe', 'OPENSSH_LICENSE', 'msys-*'], dest: 'dist/' + BASENAME + '-win32-x64/resources/resources' }], options: { mode: true } }, osx: { files: [{ expand: true, cwd: 'resources', src: ['terminal'], dest: '<%= OSX_FILENAME %>/Contents/Resources/resources/' }], options: { mode: true, }, }, }, // styles less: { options: { sourceMapFileInline: true, javascriptEnabled: true, }, dist: { files: { 'build/main.css': 'styles/main.less' } } }, // javascript babel: { dist: { files: [{ expand: true, cwd: 'src/', src: ['**/*.js'], dest: 'build/', }], }, }, shell: { electron: { command: electron + ' ' + 'build', options: { async: true, execOptions: { env: env, }, }, }, sign: { options: { failOnError: false }, command: [ 'codesign --deep -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>/Contents/Frameworks/*', 'codesign -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>', 'codesign -vvv --display <%= OSX_FILENAME_ESCAPED %>', 'codesign -v --verify <%= OSX_FILENAME_ESCAPED %>' ].join(' && ') }, zip: { command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> release/' + VERSION_FILENAME + '-Mac.zip' }, linux_npm: { command: 'cd build && npm install --production' }, }, clean: { release: ['build/', 'dist/'] }, compress: { windows: { options: { archive: './release/' + VERSION_FILENAME + '-Windows.zip', mode: 'zip', }, files: [{ expand: true, dot: true, cwd: './dist/Kitematic-win32-x64', src: '**/*', }], }, osx: { options: { archive: './release/' + VERSION_FILENAME + '-Mac.zip', mode: 'zip', }, files: [{ expand: true, dot: true, cwd: './dist/Kitematic-darwin-x64', src: '**/*', }], }, debian: { options: { archive: './release/' + VERSION_FILENAME + '-Ubuntu.zip', mode: 'zip', }, files: [{ expand: true, dot: true, cwd: './dist', src: '*.deb', }], }, }, // livereload watch: { options: { spawn: true }, livereload: { options: {livereload: true}, files: ['build/**/*'] }, js: { files: ['src/**/*.js'], tasks: ['newer:babel'] }, less: { files: ['styles/**/*.less'], tasks: ['less'] }, copy: { files: ['images/*', 'index.html', 'fonts/*'], tasks: ['newer:copy:dev'] } }, 'electron-packager': { build: { options: { platform: process.platform, arch: process.arch, dir: './build', out: './dist/', name: 'Kitematic', icon: './util/kitematic.png', version: packagejson['electron-version'], // set version of electron overwrite: true, } }, osxlnx: { options: { platform: 'linux', arch: 'x64', dir: './build', out: './dist/', name: 'Kitematic', version: packagejson['electron-version'], // set version of electron overwrite: true, } }, }, 'electron-installer-debian': { options: { name: BASENAME.toLowerCase(), // spaces and brackets cause linting errors productName: BASENAME.toLowerCase(), productDescription: 'Run containers through a simple, yet powerful graphical user interface.', maintainer: 'Ben French ', section: 'devel', priority: 'optional', icon: './util/kitematic.png', lintianOverrides: [ 'changelog-file-missing-in-native-package', 'executable-not-elf-or-script', 'extra-license-file', 'non-standard-dir-perm', 'non-standard-file-perm', 'non-standard-executable-perm', 'script-not-executable', 'shlib-with-executable-bit', 'binary-without-manpage', 'debian-changelog-file-missing', 'unusual-interpreter', 'wrong-path-for-interpreter', 'backup-file-in-package', 'package-contains-vcs-control-file', 'embedded-javascript-library', 'embedded-library', 'arch-dependent-file-in-usr-share' ], categories: [ 'Utility' ], }, linux64: { options: { arch: 'amd64' }, src: './dist/Kitematic-linux-x64/', dest: './dist/', rename: function (dest, src) { return OSX_OUT + '/' + VERSION_FILENAME + '_amd64.deb'; }, }, linux32: { options: { arch: 'i386' }, src: './dist/Kitematic-linux-ia32/', dest: './dist/', rename: function (dest, src) { return OSX_OUT + '/' + VERSION_FILENAME + '_i386.deb'; }, } }, 'electron-installer-redhat': { options: { productName: BASENAME, productDescription: 'Run containers through a simple, yet powerful graphical user interface.', priority: 'optional', icon: './util/kitematic.png', categories: [ 'Utilities', ], }, linux64: { options: { arch: 'x86_64', }, src: './dist/Kitematic-linux-x64/', dest: './dist/', rename: function (dest, src) { return OSX_OUT + '/' + VERSION_FILENAME + '_amd64.rpm'; }, }, linux32: { options: { arch: 'x86', }, src: './dist/Kitematic-linux-ia32/', dest: './dist/', rename: function (dest, src) { return OSX_OUT + '/' + VERSION_FILENAME + '_i386.rpm'; }, }, }, }); // Load the plugins for linux packaging grunt.loadNpmTasks('grunt-electron-packager'); grunt.loadNpmTasks('grunt-electron-installer-debian'); grunt.loadNpmTasks('grunt-electron-installer-redhat'); grunt.registerTask('build', ['newer:babel', 'less', 'newer:copy:dev']); grunt.registerTask('default', ['build', 'shell:electron', 'watch']); grunt.registerTask('release:linux', [ 'clean:release', 'build', 'shell:linux_npm', 'electron:linux', 'electron-packager:build', ]); grunt.registerTask('release:debian:x32', ['release:linux', 'electron-installer-debian:linux32', 'compress:debian']); grunt.registerTask('release:debian:x64', ['release:linux', 'electron-installer-debian:linux64', 'compress:debian']); grunt.registerTask('release:redhat:x32', ['release:linux', 'electron-installer-redhat:linux32']); grunt.registerTask('release:redhat:x64', ['release:linux', 'electron-installer-redhat:linux64']); grunt.registerTask('release:mac', [ 'clean:release', 'build', 'shell:linux_npm', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip', ]); grunt.registerTask('release:windows', [ 'clean:release', 'build', 'shell:linux_npm', 'electron:windows', 'copy:windows', 'rcedit:exes', 'compress:windows', ]); process.on('SIGINT', function () { grunt.task.run(['shell:electron:kill']); process.exit(1); }); }; ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2014-2016 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS ================================================ # Kitematic maintainers file # # This file describes who runs the docker/kitematic project and how. # This is a living document - if you see something out of date or missing, speak up! # # It is structured to be consumable by both humans and programs. # To extract its contents programmatically, use any TOML-compliant parser. # # This file is compiled into the MAINTAINERS file in docker/opensource. # [Org] [Org."Core maintainers"] people = [ "elesant", "FrenchBen", "jeffdm", "mchiang0610", ] [people] # A reference list of all people associated with the project. # All other sections should refer to people by their canonical key # in the people section. # ADD YOURSELF HERE IN ALPHABETICAL ORDER [people.elesant] Name = "Sean Li" Email = "mail@shang.li" GitHub = "elesant" [people.FrenchBen] Name = "Ben French" Email = "frenchben@docker.com" GitHub = "FrenchBen" [people.jeffdm] Name = "Jeff Morgan" Email = "jmorgan@docker.com" GitHub = "jeffdm" [people.mchiang0610] Name = "Michael Chiang" Email = "mchiang@docker.com" GitHub = "mchiang0610" ================================================ FILE: Makefile ================================================ .PHONY: docs docs-shell docs-build run VERSION := $(shell jq -r '.version' package.json) # TODO: clearly need to note pre-req's - OSX and node installed? - see contributing docs install: npm install run: install npm start release: install npm install electron-packager npm run release mv release/Kitematic-Mac.zip release/Kitematic-$(VERSION)-Mac.zip mv release/Kitematic-Ubuntu.zip release/Kitematic-$(VERSION)-Ubuntu.zip mv release/Kitematic-Windows.zip release/Kitematic-$(VERSION)-Windows.zip #zip: # docker container run --rm -it -w /to_zip -v $(PWD)/dist/Kitematic\ \(Beta\)-darwin-x64:/to_zip -v $(PWD)/dist:/out kramos/alpine-zip -r /out/Kitematic-$(VERSION)-Mac.zip . clean: -rm .DS_Store -rm -Rf build/ -rm -Rf dist/ -rm -Rf release/ -rm -Rf node_modules/ # Get the IP ADDRESS DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") HUGO_BIND_IP=0.0.0.0 # import the existing docs build cmds from docker/docker DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR)/$(DOCSDIR):/$(DOCSDIR)) DOCSPORT := 8000 GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) DOCKER_DOCS_IMAGE := kitematic-docs$(if $(GIT_BRANCH),:$(GIT_BRANCH)) DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) docs: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" \ hugo server \ --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) docs-shell: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash docs-build: docker build -t "$(DOCKER_DOCS_IMAGE)" -f docs/Dockerfile . ================================================ FILE: README.md ================================================ ### :warning: Deprecation Notice: This project and repository is now deprecated and is no longer under active development, see [the related roadmap issue](https://github.com/docker/roadmap/issues/67). Please use [Docker Desktop](https://www.docker.com/products/docker-desktop) instead where possible. [![Build Status](https://travis-ci.org/docker/kitematic.svg?branch=master)](https://travis-ci.org/docker/kitematic) [![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com) Please give us feedback on the new [Docker Desktop Dashboard](https://docs.docker.com/docker-for-mac/edge-release-notes/)! In the latest Edge release of Docker Desktop we have introduced the new [Docker Desktop Dashboard](https://docs.docker.com/docker-for-mac/edge-release-notes/). As part of this, Docker is working on providing a common user experience to developers and bringing the best Kitematic features to its Desktop customers. As a result, we plan on achieving feature parity and archiving the Docker Kitematic Project during 2020. After we archive the Kitematic Project there will be no new releases of Kitematic. ![Kitematic Screenshot](https://cloud.githubusercontent.com/assets/251292/8246120/d3ab271a-15ed-11e5-8736-9a730a27c79a.png) Kitematic is a simple application for managing Docker containers on Mac, Linux and Windows. ## Installing Kitematic [Download the latest version](https://github.com/docker/kitematic/releases) of Kitematic via the github release page. ## Documentation Kitematic's documentation and other information can be found at [http://kitematic.com/docs](http://kitematic.com/docs). ## Security Disclosure Security is very important to us. If you have any issue regarding security, please disclose the information responsibly by sending an email to security@docker.com and not by creating a github issue. ## Archive FAQ **Why are you archiving Kitematic?** We are learning from the capabilities in Kitematic and incorporating them into a common developer User experience and benefit all Docker Desktop users. **When will this happen?** Once we have reached feature parity and provided the most important capabilities from the existing Kitematic UI. We aim to achieve this and then to archive Kitematic in 2020. **What can I do if the new UI doesn't support something I need?** Tell us! Please add requests on the Kitematic repo. We need you to tell us what features you use so we can bring them across into the new UI. We are very interested in your feedback starting with the Edge release. ## Bugs and Feature Requests Have a bug? Please first read the [Issue Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your idea is not in the new UI, [please open a new issue](https://github.com/kitematic/kitematic/issues/new). If your problem is not addressed yet, [please open a new issue](https://github.com/kitematic/kitematic/issues/new). ## Community - Ask questions on our [user forum](https://forums.docker.com/c/open-source-projects/kitematic). - Follow [@Docker on Twitter](https://twitter.com/docker). ## Uninstalling **Mac** - Remove Kitematic.app - Remove any unwanted Virtual Machines in VirtualBox ```bash # remove app data rm -rf ~/Library/Application\ Support/Kitematic ``` **Windows** Open `Programs and Features` from `Control Panel` - Uninstall Kitematic - Uninstall Oracle VM VirtualBox ## Copyright and License Code released under the [Apache license](LICENSE). Images are copyrighted by [Docker, Inc](https://www.docker.com/). ================================================ FILE: ROADMAP.md ================================================ ## Kitematic Roadmap **January 2015** * Automatic updates * Stability bug fixes **Februay 2015** * Docker machine support * Front-end refactor * Starting Unit tests **March 2015** * Kitematic re-design (container centric workflow) * Docker Hub pull / search for public Docker images **April 2015** * Custom URL protocol **May 2015** * Docker Hub - sign-up/sign-in * Allow users to sign-up / sign-in to Docker Hub from Kitematic. * Docker Hub - private repo view if user is logged-in to Docker Hub account **June 2015** * Microsoft Windows alpha **July 2015** * Refactor to Flux Architecture * Stability & code quality improvements **August 2015** * Make Kitematic part of the Docker Toolbox * Stability & code quality improvements **September 2015** * Better integration with new version of Docker Hub * Stability & code quality improvements ================================================ FILE: __integration__/HubUtil-integration.js ================================================ jest.autoMockOff(); jasmine.getEnv().defaultTimeoutInterval = 60000; let hubUtil = require('../src/utils/HubUtil'); let Promise = require('bluebird'); describe('HubUtil Integration Tests', () => { describe('auth', () => { pit('successfully authenticates', () => { return new Promise((resolve) => { hubUtil.auth(process.env.INTEGRATION_USER, process.env.INTEGRATION_PASSWORD, (error, response, body) => { expect(response.statusCode).toBe(200); expect(error).toBe(null); let data = JSON.parse(body); expect(data.token).toBeTruthy(); resolve(); }); }); }); pit('provides a 401 if credentials are incorrect', () => { return new Promise((resolve) => { hubUtil.auth(process.env.INTEGRATION_USER, 'incorrectpassword', (error, response) => { expect(response.statusCode).toBe(401); resolve(); }); }); }); }); }); ================================================ FILE: __integration__/RegHubUtil-integration.js ================================================ jest.autoMockOff(); jasmine.getEnv().defaultTimeoutInterval = 60000; let _ = require('underscore'); let regHubUtil = require('../src/utils/RegHubUtil'); let hubUtil = require('../src/utils/HubUtil'); let Promise = require('bluebird'); describe('RegHubUtil Integration Tests', () => { describe('with login', () => { pit('lists private repos', () => { return new Promise((resolve) => { hubUtil.login(process.env.INTEGRATION_USER, process.env.INTEGRATION_PASSWORD, () => { regHubUtil.repos((error, repos) => { expect(_.findWhere(repos, {name: 'test_private', is_private: true})).toBeTruthy(); resolve(); }); }); }); }); pit('lists tags for a private repo', () => { return new Promise((resolve) => { hubUtil.login(process.env.INTEGRATION_USER, process.env.INTEGRATION_PASSWORD, () => { regHubUtil.tags(`${process.env.INTEGRATION_USER}/test_private`, (error, tags) => { expect(error).toBeFalsy(); expect(tags.length).toEqual(1); expect(tags[0].name).toEqual('latest'); resolve(); }); }); }); }); }); describe('public repos', () => { pit('lists repos', () => { return new Promise((resolve) => { hubUtil.login(process.env.INTEGRATION_USER, process.env.INTEGRATION_PASSWORD, () => { regHubUtil.repos((error, repos) => { expect(_.findWhere(repos, {name: 'test'})).toBeTruthy(); resolve(); }); }); }); }); pit('lists tags for a repo', () => { return new Promise((resolve) => { hubUtil.login(process.env.INTEGRATION_USER, process.env.INTEGRATION_PASSWORD, () => { regHubUtil.tags(`${process.env.INTEGRATION_USER}/test`, (error, tags) => { expect(error).toBeFalsy(); expect(tags.length).toEqual(1); expect(tags[0].name).toEqual('latest'); resolve(); }); }); }); }); }); }); ================================================ FILE: __mocks__/app.js ================================================ module.exports = { require: jest.fn(), match: jest.fn(), on: jest.fn() }; ================================================ FILE: __mocks__/electron.js ================================================ module.exports = { require: jest.fn(), match: jest.fn(), app: jest.fn(), remote: jest.fn(), dialog: jest.fn() }; ================================================ FILE: __mocks__/remote.js ================================================ module.exports = { require: jest.fn(), match: jest.fn() }; ================================================ FILE: __tests__/Util-test.js ================================================ jest.dontMock('../src/utils/Util').dontMock('console'); const util = require('../src/utils/Util'); describe('Util', () => { describe('when removing sensitive data', () => { it('filters ssh certificate data', () => { var testdata = String.raw`time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost sudo mkdir -p /var/lib/boot2docker" time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost echo \"-----BEGIN CERTIFICATE-----\nMIIC+DCCAeKgAwIBAgIRANfIbsa2M94gDY+fBiBiQBkwCwYJKoZIhvcNAQELMBIx\nEDAOBgNVBAoTB2ptb3JnYW4wHhcNMTUwNDE4MDEzODAwWhcNMTgwNDAyMDEzODAw\nWjAPMQ0wCwYDVQQKEwRkZXYyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA1yamWT0bk0pRU7eiStjiXe2jkzdeI0SdJZo+bjczkl6kzNW/FmR/OkcP8gHX\nCO3fUCWkR/+rBgz3nuM1Sy0BIUo0EMQGfx17OqIJPXO+BrpCHsXlphHmbQl5bE2Y\nF+bAsGc6WCippw/caNnIHRsb6zAZVYX2AHLYY0fwIDAQABo1AwTjAOBgNVHQ8BAf8EBAMCAKAwHQYD\nVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0R\nBAgwBocEwKhjZTALBgkqhkiG9w0BAQsDggEBAKBdD86+kl4X1VMjgGlNYnc42tWa\nbo1iDl/frxiLkfPSc2McAOm3AqX1ao+ynjqq1XTlBLPTQByu/oNZgA724LRJDfdG\nCKGUV8latW7rB1yhf/SZSmyhNjufuWlgCtbkw7Q/oPddzYuSOdDW8tVok9gMC0vL\naqKCWfVKkCmvGH+8/wPrkYmro/f0uwJ8ee+yrbBPlBE/qE+Lqcfr0YcXEDaS8CmL\nDjWg7KNFpA6M+/tFNQhplbjwRsCt7C4bzQu0aBIG5XH1Jr2HrKlLjWdmluPHWUL6\nX5Vh1bslYJzsSdBNZFWSKShZ+gtRpjtV7NynANDJPQNIRhDxAf4uDY9hA2c=\n-----END CERTIFICATE-----\n\" | sudo tee /var/lib/boot2docker/server.pem" time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: /usr/bin/VBoxManage showvminfo dev2 --machinereadable"`; expect(util.removeSensitiveData(testdata).indexOf('CERTIFICATE')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('nX5Vh1bslYJzsSdBNZFWSKShZ+gtRpjtV7NynANDJPQNIRhDxAf4uDY9hA2c')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('')).not.toEqual(-1); }); it('filters ssh private key data', () => { var testdata = String.raw`hZbuxglOtQv2AQqOp/luhZ3Y8kDs4cqRzoA1o+k+LAyjEb+Nk\nGA8=\n-----END CERTIFICATE-----\n\" | sudo tee /var/lib/boot2docker/ca.pem" time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost echo \"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1yamWT0bk0pRU7eiStjiXe2jkzdeI0SdJZo+bjczkl6kzNW/\nFmR/OkcP8gHXCO3fUCWkR/+rBgz3nuM1Sy0BIUo0EMQGfx17OqIJPXO+BrpCHsXl\nphHmbQl5bE2YF+bAsGc6WCippczQIu5bPweeAkR1WdlkhD08tHD4o1ESe09fXx5G\nXcZFfd2xQWdvAJX3fTuGBk3IMEF2fye5b69zUyVDGbTylyjKDOi9Xxdlc4y9cOPw\nzcwQFCOJiCBYlxDO0fbinA+KigCs29Dd5U3oXbloLr3JQTE/SkxFh9W5rkX8ysY4\n2h3EnR7YIBWt/caNnIHRsb6zAZVYX2AHLYY0fwIDAQABAoIBAQDKF3TTh/G59WnU\n4D2iXnyqy8gFRVG4gP+3TV3s+w8HIr1b5j6akwVqwUs5//5zVbSYPPNF6eJESbPi\nW/s4ROq10VR8lxSfHBsfJQrW3TwWZ6gp7atbxZ6Stv6F+5CsisReLmiAXJmVsn+j\nAA9Xchk6egFcxzWCfV7jAuaZyVI53cclepm/xkGjPwrfXr+nA+UMvO6DllC6IcBF\no4+O0jVtzdMecZnQk6nWxNJjurodTTQakrNAqSMgBshn48wf3N35b+p8RtTzLJ8L\nYuHkv6OKMITIazcHadjsN8icGgIGf2BJ1CRje7j0Yzow8jwY+Pet3yxKSfXED89B\nD34AEXl5AoGBANi17og+yPFOWURUrksO/QyzlOtXcQdQu8SmkUj4ACoqF0gegQIb\nC/DNMcYxJAsPPgw/t5Ws/af8DuatYguGukmekYREVjc7DS/hPWDZzeavPd95cOw0\nuMPgJE76HJ3BSYcp1f8WKcN+xDket9CF6Qz+VX5aQSUEc333V5h7D/nzAoGBAP4o\nVCvQu5eKYmDhMFSOA0+Qm3EECRqMLoH6kpEcbMjM8+kOeI0fUuE3CX8nzs7P4py/\n0IFj2Yxl578NHJOjCpbB1UKtxLkmDH42wXXzrWJXRaWXC93dh1sl0aB6qE25FtSD\nzjYh4y1DA/t6y95YRrIqC2WhIU7eigIoujmtOFJFAoGABSKiiWX7ewRhRyY+jxbG\n1lM3FzCWRBccq/dKgBEoZ9dhf9sBMZyUdttV751gfkaZMM8duZVE2YM2ky7OoPlL\nVs1EI38/D8X9dQIAY1gl8e57J92H2IETU8ju81Qn83EOHf7WzFmpGbHaUoQw1Ocn\nc6BfREQ9QPRPDFAdKkbYRRMCgYEAl44k4xvNQUhb8blWwJUOlFt+1Z26cAI3mXp5\n+94fYH4W1Fq0uDJ9kZ7oItLyF5EPaLlY9E8+YuJBl0OSTtdicROUv/Yu4Nk3ievM\n4TE1qvavqVaw1NRM6qVao3+A7Rf57S/Lv6vldBAKR+OpviSVw5gew7OZ0RYS5caz\nhcEtXKECgYAJb7t67nococm0PsRe8Xv1SQOQjetrhzwzD1PLOSC9TrzwA22/ZktZ\neu/qfvYgOPT4LkDGVCzn8J+TAcUVnIvAnJRQTsBu55uiL8YC5jZQ8E1hBf7kskMq\nh16WD19Djv3WhfBNXBxvnagDDWw5DxmiiKzSf0k3QDDoX7wjDAV1dQ==\n-----END RSA PRIVATE KEY-----\n\" | sudo tee /var/lib/boot2docker/server-key.pem" time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost echo \"-----BEGIN CERTIFICATE-----\nMIIC+DCCAeKgAwIBAgIRANfIbsa2M94gDY+fBiBiQBkwCwYJKoZIhvcNAQELMBIx\nEDAOBg`; expect(util.removeSensitiveData(testdata).indexOf('PRIVATE')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('94fYH4W1Fq0uDJ9kZ7oItLyF5EPaLlY9E8+YuJBl0OSTtdicROUv')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('')).not.toEqual(-1); }); it('filters username data', () => { var testdata = String.raw`/Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost echo`; expect(util.removeSensitiveData(testdata).indexOf('/Users/johnappleseed/')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('/Users//')).not.toEqual(-1); testdata = String.raw`/Users/some.wei-rdUsername/.docker/machine/machines/dev2/id_rsa docker@localhost echo`; expect(util.removeSensitiveData(testdata).indexOf('/Users/some.wei-rdUsername/.docker')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('/Users//.docker')).not.toEqual(-1); }); it('filters Windows username data', () => { var testdata = String.raw`C:\\Users\\johnappleseed\\.docker\\machine`; expect(util.removeSensitiveData(testdata).indexOf('johnappleseed')).toEqual(-1); expect(util.removeSensitiveData(testdata).indexOf('')).not.toEqual(-1); }); it ('returns input if empty or not a string', () => { expect(util.removeSensitiveData('')).toBe(''); expect(util.removeSensitiveData(1)).toBe(1); expect(util.removeSensitiveData(undefined)).toBe(undefined); }); }); describe('when verifying that a repo is official', () => { it('accepts official repo', () => { expect(util.isOfficialRepo('redis')).toBe(true); }); it('rejects falsy value as official repo', () => { expect(util.isOfficialRepo(undefined)).toBe(false); }); it('rejects empty repo name', () => { expect(util.isOfficialRepo('')).toBe(false); }); it('rejects repo with non official namespace', () => { expect(util.isOfficialRepo('kitematic/html')).toBe(false); }); it('rejects repo with a different registry address', () => { expect(util.isOfficialRepo('www.myregistry.com/kitematic/html')).toBe(false); }); }); }); ================================================ FILE: docs/README.md ================================================ # The docs have been moved! The documentation for Kitematic has been merged into [the general documentation repo](https://github.com/docker/docker.github.io). The docs for Kitematic are now here: https://github.com/docker/docker.github.io/tree/master/kitematic As always, the docs remain open-source and we appreciate your feedback and pull requests! ================================================ FILE: electron-builder.json ================================================ { "appId": "com.docker.kitematic", "asar": true, "directories": { "output": "./dist/" }, "files": [ { "filter": [ "!./node_modules/**/*", "!./package.json" ], "from": "./build/", "to": "." }, "packages.json" ], "linux": {}, "mac": { "category": "public.app-category.developer-tools" }, "msi": { "warningsAsErrors": false }, "productName": "Kitematic", "win": { "extraResources": "./resources/**/*", "icon": "./util/kitematic.ico", "target": [ { "target": "appx" }, { "target": "msi" } ] } } ================================================ FILE: index.html ================================================ Kitematic ================================================ FILE: jest-integration.json ================================================ { "testMatch": ["**/__integration__/**/*.js"], "transform": {".*": "/node_modules/babel-jest"}, "setupFiles": ["/util/testenv.js"], "setupTestFrameworkScriptFile": "/util/prepare.js", "unmockedModulePathPatterns": [ "babel", "core-js", "/node_modules/source-map-support" ] } ================================================ FILE: jest-unit.json ================================================ { "transform": { ".*": "/node_modules/babel-jest" }, "setupFiles": ["/util/testenv.js"], "setupTestFrameworkScriptFile": "/util/prepare.js", "unmockedModulePathPatterns": [ "alt", "stream", "tty", "net", "crypto", "babel", "bluebird", "object-assign", "underscore", "source-map-support", "/node_modules/.*JSONStream", "/node_modules/core-js" ] } ================================================ FILE: package.json ================================================ { "name": "Kitematic", "version": "0.17.13", "author": "Kitematic", "license": "Apache-2.0", "description": "Simple Docker Container management for Mac OS X, Windows and Ubuntu.", "homepage": "https://kitematic.com/", "main": "browser.js", "repository": { "type": "git", "url": "git@github.com:kitematic/kitematic.git" }, "bugs": "https://github.com/kitematic/kitematic/issues", "engines": { "node": "<10.0.0" }, "scripts": { "build": "tsc && npm run tslint", "integration": "jest -c jest-integration.json", "prestart": "npm run build", "release:debian:x32": "grunt release:debian:x32", "release:debian:x64": "grunt release:debian:x64", "release:redhat:x32": "grunt release:redhat:x32", "release:redhat:x64": "grunt release:redhat:x64", "release:mac": "grunt release:mac", "release:windows": "grunt release:windows", "start": "grunt", "start-dev": "NODE_ENV=development grunt", "test": "jest -c jest-unit.json", "tslint": "tslint --fix --project ./tsconfig.json" }, "electron-version": "7.2.4", "dependencies": { "JSONStream": "1.3.2", "alt": "0.16.10", "ansi-to-html": "0.3.0", "any-promise": "0.1.0", "async": "1.5.2", "babel-polyfill": "^6.26.0", "bluebird": "3.5.1", "bugsnag-js": "2.5.0", "cached-request": "1.1.2", "classnames": "2.2.5", "deep-extend": "^0.6.0", "dockerode": "3.0.1", "bl": "^1.2.3", "install": "0.1.8", "jquery": "^3.5.0", "mixpanel": "kitematic/mixpanel-node", "mkdirp": "0.5.1", "node-uuid": "1.4.8", "numeral": "1.5.6", "object-assign": "4.1.1", "osx-release": "1.1.0", "parseUri": "1.2.3-2", "react": "0.14.0", "react-bootstrap": "0.20.3", "react-retina-image": "1.3.3", "react-router": "0.13.6", "request": "^2.88.0", "request-progress": "0.3.1", "rimraf": "2.6.2", "underscore": "1.8.3", "validator": "4.9.0", "which": "1.3.0" }, "devDependencies": { "@types/react": "16.0.38", "acorn": "^5.7.4", "babel-cli": "^6.26.0", "babel-jest": "^23.6.0", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.7.0", "babel-preset-react": "^6.24.1", "braces": "^2.3.1", "decompress-zip": "^0.3.2", "electron": "^7.2.4", "electron-packager": "^12.1.1", "eslint": "^4.18.2", "eslint-plugin-react": "3.16.1", "grunt": "^1.0.3", "grunt-babel": "^7.0.0", "grunt-chmod": "1.1.1", "grunt-cli": "^1.3.1", "grunt-contrib-clean": "^2.0.0", "grunt-contrib-compress": "^1.5.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-less": "^2.0.0", "grunt-contrib-watch": "^1.1.0", "grunt-electron": "^11.0.0", "grunt-electron-installer": "^2.1.0", "grunt-electron-packager": "0.2.1", "grunt-if-missing": "1.0.1", "grunt-newer": "1.3.0", "grunt-plistbuddy": "^0.2.0", "grunt-rcedit": "^0.7.0", "grunt-shell": "^2.1.0", "handlebars": "^4.5.3", "jest-cli": "^23.6.0", "js-yaml": "^3.13.1", "load-grunt-tasks": "^4.0.0", "lodash": "^4.17.19", "lodash.template": "^4.5.0", "merge": ">=1.2.1", "minimatch": ">=3.0.4", "minimist": "1.2.3", "mixin-deep": "^1.3.2", "run-sequence": "^2.2.1", "set-value": "^2.0.1", "shell-escape": "0.2.0", "source-map-support": "0.3.3", "tslint": "^5.11.0", "typescript": "2.7.2", "yargs-parser": "^13.1.2" }, "optionalDependencies": { "grunt-electron-installer-debian": "0.3.1", "grunt-electron-installer-redhat": "0.3.1" } } ================================================ FILE: resources/MSYS_LICENSE ================================================ Kitematic includes (but does not link to) various DLLs included with the msysgit Git-1.9.5-preview20150319 distribution. Included is the MSYS runtime license. Source is available online at https://github.com/msysgit/git/tree/v1.9.5.msysgit.1 File: MSYS_LICENSE Copyright (C): 2001, Earnie Boyd File $Revision$ File Revision $Date$ MSYS Release: 1.0.2 MSYS Release Date: November 30th, 2001 The software, both source and binary forms, are covered via differing licenses. Each license has it's own set of rules so please make sure you read them carefully to see how it applies to you, particularly if you're going to distribute the software. The MSYS runtime software source can found in the winsup/cygwin directory. The existing code portions of this source is covered by the CYGWIN_LICENSE which can be found in this directory in a file by the name of CYGWIN_LICENSE. MSYS specific software code added regardless of existing license is covered by the ESPL which can be found in a file by the same name. ================================================ FILE: resources/OPENSSH_LICENSE ================================================ Kitematic includes OpenSSH ssh.exe from the msysgit distribution version Git-1.9.5-preview20150319, available online at https://github.com/msysgit/git/tree/v1.9.5.msysgit.1 This file is part of the OpenSSH software. The licences which components of this software fall under are as follows. First, we will summarize and say that all components are under a BSD licence, or a licence more free than that. OpenSSH contains no GPL code. 1) * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland * All rights reserved * * As far as I am concerned, the code I have written for this software * can be used freely for any purpose. Any derived versions of this * software must be clearly marked as such, and if the derived work is * incompatible with the protocol description in the RFC file, it must be * called by a name other than "ssh" or "Secure Shell". [Tatu continues] * However, I am not implying to give any licenses to any patents or * copyrights held by third parties, and the software includes parts that * are not under my direct control. As far as I know, all included * source code is used in accordance with the relevant license agreements * and can be used freely for any purpose (the GNU license being the most * restrictive); see below for details. [However, none of that term is relevant at this point in time. All of these restrictively licenced software components which he talks about have been removed from OpenSSH, i.e., - RSA is no longer included, found in the OpenSSL library - IDEA is no longer included, its use is deprecated - DES is now external, in the OpenSSL library - GMP is no longer used, and instead we call BN code from OpenSSL - Zlib is now external, in a library - The make-ssh-known-hosts script is no longer included - TSS has been removed - MD5 is now external, in the OpenSSL library - RC4 support has been replaced with ARC4 support from OpenSSL - Blowfish is now external, in the OpenSSL library [The licence continues] Note that any information and cryptographic algorithms used in this software are publicly available on the Internet and at any major bookstore, scientific library, and patent office worldwide. More information can be found e.g. at "http://www.cs.hut.fi/crypto". The legal status of this program is some combination of all these permissions and restrictions. Use only at your own responsibility. You will be responsible for any legal consequences yourself; I am not making any claims whether possessing or using this is legal or not in your country, and I am not taking any responsibility on your behalf. NO WARRANTY BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 2) The 32-bit CRC compensation attack detector in deattack.c was contributed by CORE SDI S.A. under a BSD-style license. * Cryptographic attack detector for ssh - source code * * Copyright (c) 1998 CORE SDI S.A., Buenos Aires, Argentina. * * All rights reserved. Redistribution and use in source and binary * forms, with or without modification, are permitted provided that * this copyright notice is retained. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL CORE SDI S.A. BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR * CONSEQUENTIAL DAMAGES RESULTING FROM THE USE OR MISUSE OF THIS * SOFTWARE. * * Ariel Futoransky * 3) ssh-keyscan was contributed by David Mazieres under a BSD-style license. * Copyright 1995, 1996 by David Mazieres . * * Modification and redistribution in source and binary forms is * permitted provided that due credit is given to the author and the * OpenBSD project by leaving this copyright notice intact. 4) The Rijndael implementation by Vincent Rijmen, Antoon Bosselaers and Paulo Barreto is in the public domain and distributed with the following license: * @version 3.0 (December 2000) * * Optimised ANSI C code for the Rijndael cipher (now AES) * * @author Vincent Rijmen * @author Antoon Bosselaers * @author Paulo Barreto * * This code is hereby placed in the public domain. * * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 5) One component of the ssh source code is under a 3-clause BSD license, held by the University of California, since we pulled these parts from original Berkeley code. * Copyright (c) 1983, 1990, 1992, 1993, 1995 * The Regents of the University of California. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. 6) Remaining components of the software are provided under a standard 2-term BSD licence with the following names as copyright holders: Markus Friedl Theo de Raadt Niels Provos Dug Song Aaron Campbell Damien Miller Kevin Steves Daniel Kouril Wesley Griffin Per Allansson Nils Nordman Simon Wilkinson * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: resources/terminal ================================================ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" CMD="clear && $*" ITERM_EXISTS=`osascript < /dev/null < /dev/null < /dev/null < favoriteName !== name); } else { favorites = [...favorites, name]; } localStorage.setItem('containers.favorites', JSON.stringify(favorites)); this.dispatch({name}); } } export default alt.createActions(ContainerActions); ================================================ FILE: src/actions/ContainerServerActions.js ================================================ import alt from '../alt'; class ContainerServerActions { constructor () { this.generateActions( 'added', 'allUpdated', 'destroyed', 'error', 'muted', 'pending', 'progress', 'started', 'unmuted', 'updated', 'waiting', 'kill', 'stopped', 'log', 'logs', 'toggleFavorite' ); } } export default alt.createActions(ContainerServerActions); ================================================ FILE: src/actions/ImageActions.js ================================================ import alt from '../alt'; import dockerUtil from '../utils/DockerUtil'; class ImageActions { all () { this.dispatch({}); dockerUtil.refresh(); } destroy (image) { dockerUtil.removeImage(image); } } export default alt.createActions(ImageActions); ================================================ FILE: src/actions/ImageServerActions.js ================================================ import alt from '../alt'; class ImageServerActions { constructor () { this.generateActions( 'added', 'updated', 'destroyed', 'error' ); } } export default alt.createActions(ImageServerActions); ================================================ FILE: src/actions/NetworkActions.js ================================================ import alt from '../alt'; class NetworkActions { constructor () { this.generateActions( 'updated', 'error', 'pending', 'clearPending' ); } } export default alt.createActions(NetworkActions); ================================================ FILE: src/actions/RepositoryActions.js ================================================ import alt from '../alt'; import regHubUtil from '../utils/RegHubUtil'; class RepositoryActions { recommended () { this.dispatch({}); regHubUtil.recommended(); } search (query, page = 1) { this.dispatch({query, page}); regHubUtil.search(query, page); } repos () { this.dispatch({}); regHubUtil.repos(); } } export default alt.createActions(RepositoryActions); ================================================ FILE: src/actions/RepositoryServerActions.js ================================================ import alt from '../alt'; class RepositoryServerActions { constructor () { this.generateActions( 'reposLoading', 'resultsUpdated', 'recommendedUpdated', 'reposUpdated', 'error' ); } } export default alt.createActions(RepositoryServerActions); ================================================ FILE: src/actions/SetupActions.js ================================================ import alt from '../alt'; import setupUtil from '../utils/SetupUtil'; class SetupActions { retry (removeVM) { this.dispatch({removeVM}); setupUtil.retry(removeVM); } useVbox () { this.dispatch({}); setupUtil.useVbox(); } } export default alt.createActions(SetupActions); ================================================ FILE: src/actions/SetupServerActions.js ================================================ import alt from '../alt'; class SetupServerActions { constructor () { this.generateActions( 'progress', 'error', 'started' ); } } export default alt.createActions(SetupServerActions); ================================================ FILE: src/actions/TagActions.js ================================================ import alt from '../alt'; import regHubUtil from '../utils/RegHubUtil'; class TagActions { tags (repo) { this.dispatch({repo}); regHubUtil.tags(repo); } localTags (repo, tags) { this.dispatch({repo, tags}); } } export default alt.createActions(TagActions); ================================================ FILE: src/actions/TagServerActions.js ================================================ import alt from '../alt'; class TagServerActions { constructor () { this.generateActions( 'tagsUpdated', 'error' ); } } export default alt.createActions(TagServerActions); ================================================ FILE: src/alt.js ================================================ import Alt from 'alt'; export default new Alt(); ================================================ FILE: src/app.js ================================================ import 'babel-polyfill'; import electron from 'electron'; const remote = electron.remote; const Menu = remote.Menu; // ipcRenderer is used as we're in the process const ipcRenderer = electron.ipcRenderer; import React from 'react'; import Promise from 'bluebird'; import metrics from './utils/MetricsUtil'; import template from './menutemplate'; import webUtil from './utils/WebUtil'; import hubUtil from './utils/HubUtil'; import setupUtil from './utils/SetupUtil'; import docker from './utils/DockerUtil'; import hub from './utils/HubUtil'; import Router from 'react-router'; import routes from './routes'; import routerContainer from './router'; import repositoryActions from './actions/RepositoryActions'; import machine from './utils/DockerMachineUtil'; Promise.config({cancellation: true}); hubUtil.init(); if (hubUtil.loggedin()) { repositoryActions.repos(); } repositoryActions.recommended(); webUtil.addWindowSizeSaving(); webUtil.addLiveReload(); webUtil.addBugReporting(); webUtil.disableGlobalBackspace(); Menu.setApplicationMenu(Menu.buildFromTemplate(template())); metrics.track('Started App'); metrics.track('app heartbeat'); setInterval(function () { metrics.track('app heartbeat'); }, 14400000); var router = Router.create({ routes: routes }); router.run(Handler => React.render(, document.body)); routerContainer.set(router); setupUtil.setup().then(() => { Menu.setApplicationMenu(Menu.buildFromTemplate(template())); docker.init(); if (!hub.prompted() && !hub.loggedin()) { router.transitionTo('login'); } else { router.transitionTo('search'); } }).catch(err => { metrics.track('Setup Failed', { step: 'catch', message: err.message }); throw err; }); ipcRenderer.on('application:quitting', () => { docker.detachEvent(); if (localStorage.getItem('settings.closeVMOnQuit') === 'true') { machine.stop(); } }); window.onbeforeunload = function () { docker.detachEvent(); }; ================================================ FILE: src/browser.js ================================================ import electron from 'electron'; const app = electron.app; const BrowserWindow = electron.BrowserWindow; import fs from 'fs'; import os from 'os'; import path from 'path'; import child_process from 'child_process'; let Promise = require('bluebird'); process.env.NODE_PATH = path.join(__dirname, 'node_modules'); process.env.RESOURCES_PATH = path.join(__dirname, '/../resources'); if (process.platform !== 'win32') { process.env.PATH = '/usr/local/bin:' + process.env.PATH; } var exiting = false; var size = {}, settingsjson = {}; try { size = JSON.parse(fs.readFileSync(path.join(app.getPath('userData'), 'size'))); } catch (err) {} try { settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, 'settings.json'), 'utf8')); } catch (err) {} app.on('ready', function () { var mainWindow = new BrowserWindow({ width: size.width || 1080, height: size.height || 680, minWidth: os.platform() === 'win32' ? 400 : 700, minHeight: os.platform() === 'win32' ? 260 : 500, 'standard-window': false, resizable: true, frame: false, backgroundColor: '#fff', show: false, webPreferences: { nodeIntegration: true, }, }); if (process.env.NODE_ENV === 'development') { mainWindow.openDevTools({mode: 'detach'}); } mainWindow.loadURL(path.normalize('file://' + path.join(__dirname, 'index.html'))); app.on('activate', function () { if (mainWindow) { mainWindow.show(); } return false; }); if (os.platform() === 'win32' || os.platform() === 'linux') { mainWindow.on('close', function (e) { mainWindow.webContents.send('application:quitting'); if(!exiting){ Promise.delay(1000).then(function(){ mainWindow.close(); }); exiting = true; e.preventDefault(); } }); app.on('window-all-closed', function () { app.quit(); }); } else if (os.platform() === 'darwin') { app.on('before-quit', function () { mainWindow.webContents.send('application:quitting'); }); } mainWindow.webContents.on('new-window', function (e) { e.preventDefault(); }); mainWindow.webContents.on('will-navigate', function (e, url) { if (url.indexOf('build/index.html#') < 0) { e.preventDefault(); } }); mainWindow.webContents.on('did-finish-load', function () { mainWindow.setTitle('Kitematic'); mainWindow.show(); mainWindow.focus(); }); }); ================================================ FILE: src/components/About.react.js ================================================ import React from 'react/addons'; import metrics from '../utils/MetricsUtil'; import utils from '../utils/Util'; import Router from 'react-router'; import RetinaImage from 'react-retina-image'; var packages; try { packages = utils.packagejson(); } catch (err) { packages = {}; } var Preferences = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return { metricsEnabled: metrics.enabled() }; }, handleGoBackClick: function () { this.goBack(); metrics.track('Went Back From About'); }, render: function () { return (
Go Back

Docker {packages.name}

{packages.version}

Kitematic is built with:

Docker Engine

Docker Machine

{packages["docker-machine-version"]}

Third-Party Software

VirtualBox

{packages["virtualbox-version"]}

Electron

{packages["electron-version"]}

); } }); module.exports = Preferences; ================================================ FILE: src/components/Account.react.js ================================================ import React from 'react/addons'; import Router from 'react-router'; import RetinaImage from 'react-retina-image'; import Header from './Header.react'; import metrics from '../utils/MetricsUtil'; import accountStore from '../stores/AccountStore'; import accountActions from '../actions/AccountActions'; module.exports = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return accountStore.getState(); }, componentDidMount: function () { document.addEventListener('keyup', this.handleDocumentKeyUp, false); accountStore.listen(this.update); }, componentWillUnmount: function () { document.removeEventListener('keyup', this.handleDocumentKeyUp, false); accountStore.unlisten(this.update); }, componentWillUpdate: function (nextProps, nextState) { if (!this.state.username && nextState.username) { if (nextState.prompted) { this.goBack(); } else { this.transitionTo('search'); } } }, handleSkip: function () { accountActions.skip(); this.transitionTo('search'); metrics.track('Skipped Login'); }, handleClose: function () { this.goBack(); metrics.track('Closed Login'); }, update: function () { this.setState(accountStore.getState()); }, render: function () { let close = this.state.prompted ? Close : Skip For Now; return (
{close}

Connect to Docker Hub

Pull and run private Docker Hub images by connecting your Docker Hub account to Kitematic.

); } }); ================================================ FILE: src/components/AccountLogin.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import Router from 'react-router'; import validator from 'validator'; import accountActions from '../actions/AccountActions'; import metrics from '../utils/MetricsUtil'; import {shell} from 'electron'; module.exports = React.createClass({ mixins: [Router.Navigation, React.addons.LinkedStateMixin], getInitialState: function () { return { username: '', password: '', errors: {} }; }, componentDidMount: function () { React.findDOMNode(this.refs.usernameInput).focus(); }, componentWillReceiveProps: function (nextProps) { this.setState({errors: nextProps.errors}); }, validate: function () { let errors = {}; if (validator.isEmail(this.state.username)) { errors.username = 'Must be a valid username (not an email)'; } else if (!validator.isLowercase(this.state.username) || !validator.isAlphanumeric(this.state.username) || !validator.isLength(this.state.username, 4, 30)) { errors.username = 'Must be 4-30 lower case letters or numbers'; } if (!validator.isLength(this.state.password, 5)) { errors.password = 'Must be at least 5 characters long'; } return errors; }, handleBlur: function () { this.setState({errors: _.omit(this.validate(), (val, key) => !this.state[key].length)}); }, handleLogin: function () { let errors = this.validate(); this.setState({errors}); if (_.isEmpty(errors)) { accountActions.login(this.state.username, this.state.password); metrics.track('Clicked Log In'); } }, handleClickSignup: function () { if (!this.props.loading) { this.replaceWith('signup'); metrics.track('Switched to Sign Up'); } }, handleClickForgotPassword: function () { shell.openExternal('https://hub.docker.com/reset-password/'); }, render: function () { let loading = this.props.loading ?
: null; return (

{this.state.errors.username}

{this.state.errors.password}

Forgot your password?

{this.state.errors.detail}

{loading}

Don't have an account yet? Sign Up
); } }); ================================================ FILE: src/components/AccountSignup.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import Router from 'react-router'; import validator from 'validator'; import accountActions from '../actions/AccountActions'; import metrics from '../utils/MetricsUtil'; module.exports = React.createClass({ mixins: [Router.Navigation, React.addons.LinkedStateMixin], getInitialState: function () { return { username: '', password: '', email: '', subscribe: true, errors: {} }; }, componentDidMount: function () { React.findDOMNode(this.refs.usernameInput).focus(); }, componentWillReceiveProps: function (nextProps) { this.setState({errors: nextProps.errors}); }, validate: function () { let errors = {}; if (!validator.isLowercase(this.state.username) || !validator.isAlphanumeric(this.state.username) || !validator.isLength(this.state.username, 4, 30)) { errors.username = 'Must be 4-30 lower case letters or numbers'; } if (!validator.isLength(this.state.password, 5)) { errors.password = 'Must be at least 5 characters long'; } if (!validator.isEmail(this.state.email)) { errors.email = 'Must be a valid email address'; } return errors; }, handleBlur: function () { this.setState({errors: _.omit(this.validate(), (val, key) => !this.state[key].length)}); }, handleSignUp: function () { let errors = this.validate(); this.setState({errors}); if (_.isEmpty(errors)) { accountActions.signup(this.state.username, this.state.password, this.state.email, this.state.subscribe); metrics.track('Clicked Sign Up'); } }, handleClickLogin: function () { if (!this.props.loading) { this.replaceWith('login'); metrics.track('Switched to Log In'); } }, render: function () { let loading = this.props.loading ?
: null; return (

{this.state.errors.username}

{this.state.errors.email}

{this.state.errors.password}

{this.state.errors.detail}

{loading}

Already have an account? Log In
); } }); ================================================ FILE: src/components/ContainerDetails.react.js ================================================ import React from 'react/addons'; import Router from 'react-router'; import ContainerDetailsHeader from './ContainerDetailsHeader.react'; import ContainerDetailsSubheader from './ContainerDetailsSubheader.react'; import containerUtil from '../utils/ContainerUtil'; import util from '../utils/Util'; import _ from 'underscore'; var ContainerDetails = React.createClass({ contextTypes: { router: React.PropTypes.func }, render: function () { if (!this.props.container) { return false; } let ports = containerUtil.ports(this.props.container); let defaultPort = _.find(_.keys(ports), port => { return util.webPorts.indexOf(port) !== -1; }); return (
); } }); module.exports = ContainerDetails; ================================================ FILE: src/components/ContainerDetailsHeader.react.js ================================================ import React from 'react/addons'; var ContainerDetailsHeader = React.createClass({ render: function () { var state; if (!this.props.container) { return false; } if (this.props.container.State.Updating) { state = UPDATING; } else if (this.props.container.State.Stopping) { state = STOPPING; } else if (this.props.container.State.Paused) { state = PAUSED; } else if (this.props.container.State.Restarting) { state = RESTARTING; } else if (this.props.container.State.Running && !this.props.container.State.ExitCode) { state = RUNNING; } else if (this.props.container.State.Starting) { state = STARTING; } else if (this.props.container.State.Downloading) { state = DOWNLOADING; } else { state = STOPPED; } return (
{this.props.container.Name}{state}
); } }); module.exports = ContainerDetailsHeader; ================================================ FILE: src/components/ContainerDetailsSubheader.react.js ================================================ import _ from 'underscore'; import React from 'react'; import {shell} from 'electron'; import metrics from '../utils/MetricsUtil'; import ContainerUtil from '../utils/ContainerUtil'; import classNames from 'classnames'; import containerActions from '../actions/ContainerActions'; import dockerMachineUtil from '../utils/DockerMachineUtil'; var ContainerDetailsSubheader = React.createClass({ contextTypes: { router: React.PropTypes.func }, disableRun: function () { if (!this.props.container) { return true; } return (!this.props.container.State.Running || !this.props.defaultPort || this.props.container.State.Updating); }, disableRestart: function () { if (!this.props.container) { return true; } return (this.props.container.State.Stopping || this.props.container.State.Downloading || this.props.container.State.Restarting || this.props.container.State.Updating); }, disableStop: function () { if (!this.props.container) { return true; } return (this.props.container.State.Stopping || this.props.container.State.Downloading || this.props.container.State.ExitCode || !this.props.container.State.Running || this.props.container.State.Updating); }, disableStart: function () { if (!this.props.container) { return true; } return (this.props.container.State.Downloading || this.props.container.State.Running || this.props.container.State.Updating); }, disableTerminal: function () { if (!this.props.container) { return true; } return (this.props.container.State.Stopping || !this.props.container.State.Running || this.props.container.State.Updating); }, disableTab: function () { if (!this.props.container) { return false; } return (this.props.container.State.Downloading); }, showHome: function () { if (!this.disableTab()) { metrics.track('Viewed Home', { from: 'header' }); this.context.router.transitionTo('containerHome', {name: this.context.router.getCurrentParams().name}); } }, showSettings: function () { if (!this.disableTab()) { metrics.track('Viewed Settings'); this.context.router.transitionTo('containerSettings', {name: this.context.router.getCurrentParams().name}); } }, handleRun: function () { if (this.props.defaultPort && !this.disableRun()) { metrics.track('Opened In Browser', { from: 'header' }); shell.openExternal(this.props.ports[this.props.defaultPort].url); } }, handleRestart: function () { if (!this.disableRestart()) { metrics.track('Restarted Container'); containerActions.restart(this.props.container.Name); } }, handleStop: function () { if (!this.disableStop()) { metrics.track('Stopped Container'); containerActions.stop(this.props.container.Name); } }, handleStart: function () { if (!this.disableStart()) { metrics.track('Started Container'); containerActions.start(this.props.container.Name); } }, handleDocs: function () { let repoUri = 'https://hub.docker.com/r/'; let imageName = this.props.container.Config.Image.split(':')[0]; if (imageName.indexOf('/') === -1) { repoUri = repoUri + 'library/' + imageName; } else { repoUri = repoUri + imageName; } shell.openExternal(repoUri); }, handleTerminal: function () { if (!this.disableTerminal()) { metrics.track('Terminaled Into Container'); var container = this.props.container; var shell = ContainerUtil.env(container).reduce((envs, env) => { envs[env[0]] = env[1]; return envs; }, {}).SHELL; if(!shell) { shell = localStorage.getItem('settings.terminalShell') || 'sh'; } dockerMachineUtil.dockerTerminal(`docker exec -it ${this.props.container.Name} ${shell}`); } }, render: function () { var restartActionClass = classNames({ action: true, disabled: this.disableRestart() }); var stopActionClass = classNames({ action: true, disabled: this.disableStop() }); var startActionClass = classNames({ action: true, disabled: this.disableStart() }); var terminalActionClass = classNames({ action: true, disabled: this.disableTerminal() }); var docsActionClass = classNames({ action: true, disabled: false }); var currentRoutes = _.map(this.context.router.getCurrentRoutes(), r => r.name); var currentRoute = _.last(currentRoutes); var tabHomeClasses = classNames({ 'details-tab': true, 'active': currentRoute === 'containerHome', disabled: this.disableTab() }); var tabSettingsClasses = classNames({ 'details-tab': true, 'active': currentRoutes && (currentRoutes.indexOf('containerSettings') >= 0), disabled: this.disableTab() }); var startStopToggle; if (this.disableStop()) { startStopToggle = (
START
); } else { startStopToggle = (
STOP
); } return (
{startStopToggle}
RESTART
EXEC
DOCS
Home Settings
); } }); module.exports = ContainerDetailsSubheader; ================================================ FILE: src/components/ContainerHome.react.js ================================================ import _ from 'underscore'; import $ from 'jquery'; import React from 'react/addons'; import ContainerProgress from './ContainerProgress.react'; import ContainerHomeLogs from './ContainerHomeLogs.react'; import ContainerHomeFolders from './ContainerHomeFolders.react'; import {shell} from 'electron'; var ContainerHome = React.createClass({ contextTypes: { router: React.PropTypes.func }, componentDidMount: function () { this.handleResize(); window.addEventListener('resize', this.handleResize); }, componentWillUnmount: function () { window.removeEventListener('resize', this.handleResize); }, componentDidUpdate: function () { this.handleResize(); }, handleResize: function () { $('.full .wrapper').height(window.innerHeight - 132); $('.left .wrapper').height(window.innerHeight - 132); $('.right .wrapper').height(window.innerHeight / 2 - 55); }, handleErrorClick: function () { // Display wiki for proxy: https://github.com/docker/kitematic/wiki/Common-Proxy-Issues-&-Fixes shell.openExternal('https://github.com/kitematic/kitematic/issues/new'); }, showFolders: function () { return this.props.container.Mounts && this.props.container.Mounts.length > 0 && this.props.container.State.Running; }, render: function () { if (!this.props.container) { return ''; } let body; if (this.props.container.Error) { let error = this.props.container.Error.message; if (!error) { error = this.props.container.Error; } else { if (error.indexOf('ETIMEDOUT') !== -1) { error = 'Timeout error - Try and restart your VM by running: \n"docker-machine restart default" in a terminal'; } if (error.indexOf('ECONNREFUSED') !== -1) { error = 'Is your VM up and running? Check that "docker ps" works in a terminal.'; } } body = (

We're sorry. There seems to be an error:

{error.split('\n').map(i => { return

{i}

; })}

If this error is invalid, please file a ticket on our Github repo.

File Ticket
); } else if (this.props.container && this.props.container.State.Downloading) { if (this.props.container.Progress) { let values = []; let sum = 0.0; for (let i = 0; i < this.props.container.Progress.amount; i++) { values.push(Math.round(this.props.container.Progress.progress[i].value)); sum += this.props.container.Progress.progress[i].value; } sum = sum / this.props.container.Progress.amount; if (isNaN(sum)) { sum = 0; } let total = (Math.round(sum * 100) / 100).toFixed(2); body = (

{total >= 100 ? 'Creating Container' : 'Downloading Image'}

{total}%

); } else if (this.props.container.State.Waiting) { body = (

Waiting For Another Download

); } else { body = (

Connecting to Docker Hub

); } } else { var logWidget = ( ); var folderWidget; if (this.showFolders()) { folderWidget = ( ); } if (logWidget && !folderWidget) { body = (
{logWidget}
); } else { body = (
{logWidget}
{folderWidget}
); } } return body; } }); module.exports = ContainerHome; ================================================ FILE: src/components/ContainerHomeFolders.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import RetinaImage from 'react-retina-image'; import path from 'path'; import {shell} from 'electron'; import util from '../utils/Util'; import metrics from '../utils/MetricsUtil'; import containerActions from '../actions/ContainerActions'; import electron from 'electron'; const remote = electron.remote; const dialog = remote.dialog; import mkdirp from 'mkdirp'; var ContainerHomeFolder = React.createClass({ contextTypes: { router: React.PropTypes.func }, handleClickFolder: function (source, destination) { metrics.track('Opened Volume Directory', { from: 'home' }); if (source.indexOf(util.windowsToLinuxPath(util.home())) === -1) { dialog.showMessageBox({ message: `Enable all volumes to edit files? This may not work with all database containers.`, buttons: ['Enable Volumes', 'Cancel'] }).then(({response}) => { if (response === 0) { var mounts = _.clone(this.props.container.Mounts); var newSource = path.join(util.home(), util.documents(), 'Kitematic', this.props.container.Name, destination); mounts.forEach(m => { if (m.Destination === destination) { m.Source = util.windowsToLinuxPath(newSource); m.Driver = null; } }); mkdirp(newSource, function (err) { console.log(err); if (!err) { shell.showItemInFolder(newSource); } }); let binds = mounts.map(m => { return m.Source + ':' + m.Destination; }); let hostConfig = _.extend(this.props.container.HostConfig, {Binds: binds}); containerActions.update(this.props.container.Name, {Mounts: mounts, HostConfig: hostConfig}); } }); } else { let path = util.isWindows() ? util.linuxToWindowsPath(source) : source; shell.showItemInFolder(path); } }, handleClickChangeFolders: function () { metrics.track('Viewed Volume Settings', { from: 'preview' }); this.context.router.transitionTo('containerSettingsVolumes', {name: this.context.router.getCurrentParams().name}); }, render: function () { if (!this.props.container) { return false; } var folders = _.map(this.props.container.Mounts, (m, i) => { let destination = m.Destination; let source = m.Source; return (
{destination}
); }); return (
Volumes
{folders}
); } }); module.exports = ContainerHomeFolder; ================================================ FILE: src/components/ContainerHomeIpPortsPreview.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; var ContainerHomeIpPortsPreview = React.createClass({ handleClickPortSettings: function () { this.props.handleClickPortSettings(); }, render: function () { var ports = _.map(_.pairs(this.props.ports), pair => { var key = pair[0]; var val = pair[1]; return ( {key + '/' + val.portType} {val.url} ); }); return (
IP & PORTS

You can access this container using the following IP address and port:

{ports}
DOCKER PORT ACCESS URL
); } }); module.exports = ContainerHomeIpPortsPreview; ================================================ FILE: src/components/ContainerHomeLogs.react.js ================================================ import $ from 'jquery'; import React from 'react/addons'; import Router from 'react-router'; import containerActions from '../actions/ContainerActions'; import Convert from 'ansi-to-html'; import * as fs from 'fs'; import { clipboard, remote, shell } from 'electron'; const dialog = remote.dialog; let escape = function (html) { var text = document.createTextNode(html); var div = document.createElement('div'); div.appendChild(text); return div.innerHTML; }; var FontSelect = React.createClass({ getFontSizes: function(start, end){ let options = []; for(let i = start; i<=end; i++){ options.push(); } return options; }, render: function(){ return ( ); } }); let convert = new Convert(); let prevBottom = 0; module.exports = React.createClass({ getInitialState: function(){ return { fontSize: 10, follow: true, }; }, onFontChange: function(event){ let $target = event.target; this.setState((prevState)=>({ fontSize: $target.value, follow: prevState.follow })); }, componentDidUpdate: function () { var node = $('.logs').get()[0]; if(this.state.follow){ node.scrollTop = node.scrollHeight; } }, componentWillReceiveProps: function (nextProps) { if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) { containerActions.active(nextProps.container.Name); } }, componentDidMount: function () { containerActions.active(this.props.container.Name); }, componentWillUnmount: function () { containerActions.active(null); }, toggleFollow: function () { this.setState((prevState)=>({ fontSize: prevState.fontsize, follow: !prevState.follow })); }, render: function () { let _logs = ''; let logs = this.props.container.Logs ? this.props.container.Logs.map((l, index) => { const key = `${this.props.container.Name}-${index}`; _logs = _logs.concat((l.substr(l.indexOf(' ')+1)).replace(/\[\d+m/g,'').concat('\n')); return
'))}}>
; }) : ['0 No logs for this container.']; let copyLogs = (event) => { clipboard.writeText(_logs); let btn = event.target; btn.innerHTML = 'Copied !'; btn.style.color = '#FFF'; setTimeout(()=>{ btn.style.color = 'inherit' btn.innerHTML = 'Copy'; }, 1000); }; let saveLogs = (event) => { //create default filename with timestamp let path = `${this.props.container.Name} ${new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/:/g,'-')}.txt`; dialog.showSaveDialog({ defaultPath: path }).then(({filePath}) => { if (!filePath) return; fs.writeFile(filePath, _logs, (err) => { if(!err){ shell.showItemInFolder(filePath); }else{ dialog.showErrorBox('Oops! an error occured', err.message); } }); }); }; return (
Container Logs
{logs}
); } }); ================================================ FILE: src/components/ContainerList.react.js ================================================ import React from 'react/addons'; import ContainerListItem from './ContainerListItem.react'; var ContainerList = React.createClass({ componentWillMount: function () { this.start = Date.now(); }, render: function () { var containers = this.props.containers.map(container => { return ( ); }); return (
    {containers}
); } }); module.exports = ContainerList; ================================================ FILE: src/components/ContainerListItem.react.js ================================================ import $ from 'jquery'; import React from 'react/addons'; import Router from 'react-router'; import electron from 'electron'; const remote = electron.remote; const dialog = remote.dialog; import metrics from '../utils/MetricsUtil'; import {OverlayTrigger, Tooltip} from 'react-bootstrap'; import containerActions from '../actions/ContainerActions'; var ContainerListItem = React.createClass({ toggleFavoriteContainer: function (e) { e.preventDefault(); e.stopPropagation(); containerActions.toggleFavorite(this.props.container.Name); }, handleDeleteContainer: function (e) { e.preventDefault(); e.stopPropagation(); dialog.showMessageBox({ message: 'Are you sure you want to stop & remove this container?', buttons: ['Remove', 'Cancel'] }).then(({response}) => { if (response === 0) { metrics.track('Deleted Container', { from: 'list', type: 'existing' }); containerActions.destroy(this.props.container.Name); } }); }, render: function () { var self = this; var container = this.props.container; var imageNameTokens = container.Config.Image.split('/'); var repo; if (imageNameTokens.length > 1) { repo = imageNameTokens[1]; } else { repo = imageNameTokens[0]; } var imageName = ( {container.Config.Image}}> {repo} ); // Synchronize all animations var style = { WebkitAnimationDelay: 0 + 'ms' }; var state; if (container.State.Downloading) { state = ( Downloading}>
); } else if (container.State.Running && !container.State.Paused) { state = ( Running}>
); } else if (container.State.Restarting) { state = ( Restarting}>
); } else if (container.State.Paused) { state = ( Paused}>
); } else if (container.State.ExitCode) { state = ( Stopped}>
); } else { state = ( Stopped}>
); } return (
  • {state}
    {container.Name}
    {imageName}
  • ); } }); module.exports = ContainerListItem; ================================================ FILE: src/components/ContainerProgress.react.js ================================================ import React from 'react'; /* Usage: */ var ContainerProgress = React.createClass({ render: function () { var pBar1Style = { height: this.props.pBar1 + '%' }; var pBar2Style = { height: this.props.pBar2 + '%' }; var pBar3Style = { height: this.props.pBar3 + '%' }; var pBar4Style = { height: this.props.pBar4 + '%' }; return (
    ); } }); module.exports = ContainerProgress; ================================================ FILE: src/components/ContainerSettings.react.js ================================================ import $ from 'jquery'; import _ from 'underscore'; import React from 'react/addons'; import Router from 'react-router'; var ContainerSettings = React.createClass({ contextTypes: { router: React.PropTypes.func }, componentWillReceiveProps: function () { this.init(); }, componentDidMount: function() { this.init(); this.handleResize(); window.addEventListener('resize', this.handleResize); }, componentWillUnmount: function() { window.removeEventListener('resize', this.handleResize); }, componentDidUpdate: function () { this.handleResize(); }, handleResize: function () { $('.settings-panel').height(window.innerHeight - 210); }, init: function () { var currentRoute = _.last(this.context.router.getCurrentRoutes()).name; if (currentRoute === 'containerSettings') { this.context.router.transitionTo('containerSettingsGeneral', {name: this.context.router.getCurrentParams().name}); } }, render: function () { var container = this.props.container; if (!container) { return (
    ); } return (
    • General
    • Hostname / Ports
    • Volumes
    • Network
    • Advanced
    ); } }); module.exports = ContainerSettings; ================================================ FILE: src/components/ContainerSettingsAdvanced.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import metrics from '../utils/MetricsUtil'; import ContainerUtil from '../utils/ContainerUtil'; import containerActions from '../actions/ContainerActions'; var ContainerSettingsAdvanced = React.createClass({ mixins: [React.addons.LinkedStateMixin], contextTypes: { router: React.PropTypes.func }, getInitialState: function () { let [tty, openStdin, privileged, restartPolicy] = ContainerUtil.mode(this.props.container) || [true, true, false, {MaximumRetryCount: 0, Name: 'no'}]; return { tty: tty, openStdin: openStdin, privileged: privileged, restartPolicy: restartPolicy.Name === 'always' }; }, handleSaveAdvancedOptions: function () { metrics.track('Saved Advanced Options'); let tty = this.state.tty; let openStdin = this.state.openStdin; let privileged = this.state.privileged; let restartPolicy = this.state.restartPolicy? {MaximumRetryCount: 0, Name: 'always'} : {MaximumRetryCount: 0, Name: 'no'}; let hostConfig = _.extend(this.props.container.HostConfig, {Privileged: privileged, RestartPolicy: restartPolicy}); containerActions.update(this.props.container.Name, {Tty: tty, OpenStdin: openStdin, HostConfig: hostConfig}); }, handleChangeTty: function () { this.setState({ tty: !this.state.tty }); }, handleChangeOpenStdin: function () { this.setState({ openStdin: !this.state.openStdin }); }, handleChangePrivileged: function () { this.setState({ privileged: !this.state.privileged }); }, handleChangeRestartPolicy: function () { this.setState({ restartPolicy: !this.state.restartPolicy }); }, render: function () { if (!this.props.container) { return false; } return (

    Advanced Options

    Save
    ); } }); module.exports = ContainerSettingsAdvanced; ================================================ FILE: src/components/ContainerSettingsGeneral.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import metrics from '../utils/MetricsUtil'; import electron, { clipboard } from 'electron'; const remote = electron.remote; const dialog = remote.dialog; import ContainerUtil from '../utils/ContainerUtil'; import containerActions from '../actions/ContainerActions'; import util from '../utils/Util'; var ContainerSettingsGeneral = React.createClass({ mixins: [React.addons.LinkedStateMixin], contextTypes: { router: React.PropTypes.func }, getInitialState: function () { let env = ContainerUtil.env(this.props.container) || []; env.push(['', '']); env = _.map(env, e => { return [util.randomId(), e[0], e[1]]; }); return { slugName: null, nameError: null, copiedId: false, env: env }; }, handleNameChange: function (e) { var name = e.target.value; if (name === this.state.slugName) { return; } name = name.replace(/^\s+|\s+$/g, ''); // Trim name = name.toLowerCase(); // Remove Accents let from = "àáäâèéëêìíïîòóöôùúüûñç·/,:;"; let to = "aaaaeeeeiiiioooouuuunc-----"; for (var i=0, l=from.length ; i { let [, key, value] = kvp; if ((key && key.length) || (value && value.length)) { list.push(key + '=' + value); } }); containerActions.update(this.props.container.Name, {Env: list}); }, handleChangeEnvKey: function (index, event) { let env = _.map(this.state.env, _.clone); env[index][1] = event.target.value; this.setState({ env: env }); }, handleChangeEnvVal: function (index, event) { let env = _.map(this.state.env, _.clone); env[index][2] = event.target.value; this.setState({ env: env }); }, handleAddEnvVar: function () { let env = _.map(this.state.env, _.clone); env.push([util.randomId(), '', '']); this.setState({ env: env }); metrics.track('Added Pending Environment Variable'); }, handleRemoveEnvVar: function (index) { let env = _.map(this.state.env, _.clone); env.splice(index, 1); if (env.length === 0) { env.push([util.randomId(), '', '']); } this.setState({ env: env }); metrics.track('Removed Environment Variable'); }, handleDeleteContainer: function () { dialog.showMessageBox({ message: 'Are you sure you want to delete this container?', buttons: ['Delete', 'Cancel'] }).then(({response}) => { if (response === 0) { metrics.track('Deleted Container', { from: 'settings', type: 'existing' }); containerActions.destroy(this.props.container.Name); } }); }, render: function () { if (!this.props.container) { return false; } var clipboardStatus; var willBeRenamedAs; var btnSaveName = ( Save ); if (this.state.slugName) { willBeRenamedAs = (

    Will be renamed as: {this.state.slugName}

    ); btnSaveName = ( Save ); } else if (this.state.nameError) { willBeRenamedAs = (

    {this.state.nameError}

    ); } if (this.state.copiedId) { clipboardStatus = (

    Copied to Clipboard

    ); } let containerInfo = (

    Container Info

    ID
    Copy {clipboardStatus}
    NAME
    {btnSaveName} {willBeRenamedAs}
    ); let vars = _.map(this.state.env, (kvp, index) => { let [id, key, val] = kvp; let icon; if (index === this.state.env.length - 1) { icon = ; } else { icon = ; } return (
    {icon}
    ); }); return (
    {containerInfo}

    Environment Variables

    KEY
    VALUE
    {vars}
    Save

    Delete Container

    Delete Container
    ); } }); module.exports = ContainerSettingsGeneral; ================================================ FILE: src/components/ContainerSettingsNetwork.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import metrics from '../utils/MetricsUtil'; import docker from '../utils/DockerUtil'; import containerActions from '../actions/ContainerActions'; import networkStore from '../stores/NetworkStore'; import Router from 'react-router'; import ContainerUtil from '../utils/ContainerUtil'; import containerStore from '../stores/ContainerStore'; var ContainerSettingsNetwork = React.createClass({ mixins: [React.addons.LinkedStateMixin], contextTypes: { router: React.PropTypes.func }, getInitialState: function () { let usedNetworks = this.getUsedNetworks(networkStore.all()); var links = ContainerUtil.links(this.props.container); return { networks: networkStore.all(), error: networkStore.getState().error, pending: networkStore.getState().pending, usedNetworks, links: links, newLink: { container: "", alias: "", }, isNewLinkValid: false, containers: this.containerLinkOptions(containerStore.getState().containers) }; }, getUsedNetworks(networks) { const usedKeys = _.keys(this.props.container.NetworkSettings.Networks); return _.object(_.map(networks, function (network) { return [network.Name, _.contains(usedKeys, network.Name)]; })); }, componentDidMount: function () { networkStore.listen(this.update); }, componentWillUnmount: function () { networkStore.unlisten(this.update); }, update: function () { let newState = { networks: networkStore.all(), error: networkStore.getState().error, pending: networkStore.getState().pending }; if (!newState.pending) { newState.usedNetworks = this.getUsedNetworks(networkStore.all()); } this.setState(newState); }, handleSaveNetworkOptions: function () { metrics.track('Saved Network Options'); let connectedNetworks = []; let disconnectedNetworks = []; let containerNetworks = this.props.container.NetworkSettings.Networks; let usedNetworks = this.state.usedNetworks; _.each(networkStore.all(), network => { let isConnected = _.has(containerNetworks, network.Name); if (isConnected !== usedNetworks[network.Name]) { if (isConnected) { disconnectedNetworks.push(network.Name); } else { connectedNetworks.push(network.Name); } } }); if (connectedNetworks.length || disconnectedNetworks.length) { docker.updateContainerNetworks(this.props.container.Name, connectedNetworks, disconnectedNetworks); } }, handleToggleNetwork: function (event) { let usedNetworks = _.clone(this.state.usedNetworks); let networkName = event.target.name; let newState = !usedNetworks[networkName]; if (newState) { if (networkName === 'none') { usedNetworks = _.mapObject(usedNetworks, () => false); } else { usedNetworks['none'] = false; } } usedNetworks[networkName] = newState; this.setState({ usedNetworks }); }, handleToggleHostNetwork: function () { let NetworkingConfig = { EndpointsConfig: {} }; if (!this.state.usedNetworks.host) { NetworkingConfig.EndpointsConfig.host = {}; } containerActions.update(this.props.container.Name, {NetworkingConfig}); }, containerLinkOptions: function (containers) { const usedNetworks = _.keys(this.props.container.NetworkSettings.Networks); const currentContainerName = this.props.container.Name; return _.values(containers).filter(function(container){ var sameNetworks = _.keys(container.NetworkSettings.Networks).filter(function(network){ return _.contains(usedNetworks, network); }); if(container.State.Downloading){ // is downloading return false; }else if(container.Name == currentContainerName){ // is current container return false }else if (sameNetworks.length == 0) { // not in the same network return false; }else{ return true; } }).sort(function (a, b) { return a.Name.localeCompare(b.Name); }); }, handleNewLink: function () { let links = this.state.links; links.push({ alias: this.state.newLink.alias.trim(), container: this.state.newLink.container }); this.setState({ links, newLink: { container: "", alias: "", } }); this.saveContainerLinks(); }, handleNewLinkContainerChange: function () { let newLink = this.state.newLink; newLink.container = event.target.value; this.setState({ newLink }); this.checkNewLink(); }, handleNewLinkAliasChange: function () { let newLink = this.state.newLink; newLink.alias = event.target.value; this.setState({ newLink }); this.checkNewLink(); }, checkNewLink: function () { this.setState({ isNewLinkValid: this.state.newLink.container != "" && /[A-Za-z0-9\-]$/.test(this.state.newLink.alias) }); }, handleRemoveLink: function (event) { let links = this.state.links; links.splice( parseInt(event.target.name), 1); this.setState({ links }); this.saveContainerLinks(); }, saveContainerLinks: function () { var linksPaths = ContainerUtil.normalizeLinksPath(this.props.container, this.state.links); let hostConfig = _.extend(this.props.container.HostConfig, {Links: linksPaths}); containerActions.update(this.props.container.Name, {HostConfig: hostConfig}); }, render: function () { let isUpdating = (this.props.container.State.Updating || this.state.pending); let networks = _.map(this.state.networks, (network, index) => { if (network.Name !== 'host') { return ( {network.Name} {network.Driver} ) } }); let links = _.map(this.state.links, (link, key) => { return ( {link.container} {link.alias} OPEN REMOVE ) }) let containerOptions = _.map(this.state.containers, (container) => { return ( ) }) return (

    Configure network

    {networks}
      NAME DRIVER
    { !this.state.usedNetworks.host ? Save : null } { this.state.usedNetworks.host ? You cannot configure networks while container connected to host network : null }

    Host network

    { !this.state.usedNetworks.host ? Connect to host network : null } { this.state.usedNetworks.host ? Disconnect from host network : null }

    Links

    {links}
    NAME ALIAS  
    ); } }); module.exports = ContainerSettingsNetwork; ================================================ FILE: src/components/ContainerSettingsPorts.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import {shell} from 'electron'; import ContainerUtil from '../utils/ContainerUtil'; import containerActions from '../actions/ContainerActions'; import containerStore from '../stores/ContainerStore'; import metrics from '../utils/MetricsUtil'; import docker from '../utils/DockerUtil'; import {webPorts} from '../utils/Util'; import {DropdownButton, MenuItem} from 'react-bootstrap'; var ContainerSettingsPorts = React.createClass({ contextTypes: { router: React.PropTypes.func }, getInitialState: function () { var ports = ContainerUtil.ports(this.props.container); var initialPorts = this.props.container.InitialPorts; ports[''] = { ip: docker.host, url: '', port: '', portType: 'tcp', error: null }; return { ports: ports, initialPorts: initialPorts, hostname: this.props.container.Config.Hostname }; }, handleViewLink: function (url) { metrics.track('Opened In Browser', { from: 'settings' }); shell.openExternal('http://' + url); }, createEmptyPort: function (ports) { ports[''] = { ip: docker.host, url: '', port: '', portType: 'tcp' }; document.getElementById('portKey').value = ''; document.getElementById('portValue').value = ''; }, addPort: function () { if (document.getElementById('portKey') !== null) { var portKey = document.getElementById('portKey').value; var portValue = document.getElementById('portValue').value; var portTypeValue = document.getElementById('portType').textContent; var ports = this.state.ports; if (portKey !== '') { ports[portKey] = { ip: docker.host, url: docker.host + ':' + portValue, port: portValue, portType: portTypeValue.trim(), error: null }; this.checkPort(ports, portKey, portKey); if (ports[portKey].error === null) { this.createEmptyPort(ports); } } } return ports; }, handleAddPort: function (e) { var ports = this.addPort(); this.setState({ports: ports}); metrics.track('Added Pending Port'); }, checkPort: function (ports, port, key) { // basic validation, if number is integer, if its in range, if there // is no collision with ports of other containers and also if there is no // collision with ports for current container const otherContainers = _.filter(_.values(containerStore.getState().containers), c => c.Name !== this.props.container.Name); const otherPorts = _.flatten(otherContainers.map(container => { try { return _.values(container.NetworkSettings.Ports).map(hosts => hosts.map(host => { return {port: host.HostPort, name: container.Name}; }) ); }catch (err) { } })).reduce((prev, pair) => { try { prev[pair.port] = pair.name; }catch (err) { } return prev; }, {}); const duplicates = _.filter(ports, (v, i) => { return (i !== key && _.isEqual(v.port, port)); }); if (!port.match(/^[0-9]+$/g)) { ports[key].error = 'Needs to be an integer.'; } else if (port <= 0 || port > 65535) { ports[key].error = 'Needs to be in range <1,65535>.'; } else if (otherPorts[port]) { ports[key].error = 'Collision with container "' + otherPorts[port] + '"'; } else if (duplicates.length > 0) { ports[key].error = 'Collision with another port in this container.'; } else if (port === 22 || port === 2376) { ports[key].error = 'Ports 22 and 2376 are reserved ports for Kitematic/Docker.'; } }, handleChangePort: function (key, e) { let ports = this.state.ports; let port = e.target.value; // save updated port ports[key] = _.extend(ports[key], { url: ports[key].ip + ':' + port, port: port, error: null }); this.checkPort(ports, port, key); this.setState({ports: ports}); }, handleChangePortKey: function (key, e) { let ports = this.state.ports; let portKey = e.target.value; // save updated port var currentPort = ports[key]; delete ports[key]; ports[portKey] = currentPort; this.setState({ports: ports}); }, handleRemovePort: function (key, e) { let ports = this.state.ports; delete ports[key]; this.setState({ports: ports}); }, handleChangePortType: function (key, portType) { let ports = this.state.ports; let port = ports[key].port; // save updated port ports[key] = _.extend(ports[key], { url: ports[key].ip + ':' + port, port: port, portType: portType, error: null }); this.setState({ports: ports}); }, isInitialPort: function (key, ports) { for (var idx in ports) { if (ports.hasOwnProperty(idx)) { var p = idx.split('/'); if (p.length > 0) { if (p[0] === key) { return true; } } } } return false; }, handleChangeHostnameEnabled: function (e) { var value = e.target.value; this.setState({ hostname: value }); }, handleSave: function () { let ports = this.state.ports; ports = this.addPort(); this.setState({ports: ports}); let exposedPorts = {}; let portBindings = _.reduce(ports, (res, value, key) => { if (key !== '') { res[key + '/' + value.portType] = [{ HostPort: value.port }]; exposedPorts[key + '/' + value.portType] = {}; } return res; }, {}); let hostConfig = _.extend(this.props.container.HostConfig, {PortBindings: portBindings, Hostname: this.state.hostname}); let config = _.extend(this.props.container.Config, {Hostname: this.state.hostname}); containerActions.update(this.props.container.Name, {ExposedPorts: exposedPorts, HostConfig: hostConfig, Config: config}); }, render: function () { if (!this.props.container) { return false; } var isUpdating = (this.props.container.State.Updating); var isValid = true; var ports = _.map(_.pairs(this.state.ports), pair => { var key = pair[0]; var {ip, port, url, portType, error} = pair[1]; isValid = (error) ? false : isValid; let ipLink = (this.props.container.State.Running && !this.props.container.State.Paused && !this.props.container.State.ExitCode && !this.props.container.State.Restarting) ? ({ip}) : ({ip}); var icon = ''; var portKey = ''; var portValue = ''; if (key === '') { icon = ; portKey = ; portValue = ; }else { if (this.isInitialPort(key, this.state.initialPorts)) { icon = ; }else { icon = ; } portKey = ; portValue = ; } return ( {portKey} {ipLink}: {portValue} TCP UDP {icon} {error} ); }); return (

    Configure Hostname

    HOSTNAME

    Configure Ports

    {ports}
    DOCKER PORT PUBLISHED IP:PORT
    Save
    ); } }); module.exports = ContainerSettingsPorts; ================================================ FILE: src/components/ContainerSettingsVolumes.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import electron from 'electron'; const remote = electron.remote; const dialog = remote.dialog; import {shell} from 'electron'; import util from '../utils/Util'; import metrics from '../utils/MetricsUtil'; import containerActions from '../actions/ContainerActions'; var ContainerSettingsVolumes = React.createClass({ handleChooseVolumeClick: function (dockerVol) { dialog.showOpenDialog({properties: ['openDirectory', 'createDirectory']}).then(({filePaths}) => { if (!filePaths) { return; } var directory = filePaths[0]; if (!directory || (!util.isNative() && directory.indexOf(util.home()) === -1)) { dialog.showMessageBox({ type: 'warning', buttons: ['OK'], message: 'Invalid directory - Please make sure the directory exists and you can read/write to it.' }); return; } metrics.track('Choose Directory for Volume'); let mounts = _.clone(this.props.container.Mounts); _.each(mounts, m => { if (m.Destination === dockerVol) { m.Source = util.windowsToLinuxPath(directory); m.Driver = null; } }); let binds = mounts.map(m => { return m.Source + ':' + m.Destination; }); let hostConfig = _.extend(this.props.container.HostConfig, {Binds: binds}); containerActions.update(this.props.container.Name, {Mounts: mounts, HostConfig: hostConfig}); }); }, handleRemoveVolumeClick: function (dockerVol) { metrics.track('Removed Volume Directory', { from: 'settings' }); let mounts = _.clone(this.props.container.Mounts); _.each(mounts, m => { if (m.Destination === dockerVol) { m.Source = null; m.Driver = 'local'; } }); let binds = mounts.map(m => { return m.Source + ':' + m.Destination; }); let hostConfig = _.extend(this.props.container.HostConfig, {Binds: binds}); containerActions.update(this.props.container.Name, {Mounts: mounts, HostConfig: hostConfig}); }, handleOpenVolumeClick: function (path) { metrics.track('Opened Volume Directory', { from: 'settings' }); if (util.isWindows()) { shell.showItemInFolder(util.linuxToWindowsPath(path)); } else { shell.showItemInFolder(path); } }, render: function () { if (!this.props.container) { return false; } var homeDir = util.isWindows() ? util.windowsToLinuxPath(util.home()) : util.home(); var mounts = _.map(this.props.container.Mounts, (m, i) => { let source = m.Source, destination = m.Destination; if (!m.Source || (!util.isNative() && m.Source.indexOf(homeDir) === -1) || (m.Source.indexOf('/var/lib/docker/volumes') !== -1)) { source = ( No Folder ); } else { let local = util.isWindows() ? util.linuxToWindowsPath(source) : source; source = ( {local.replace(process.env.HOME, '~')} ); } return ( {destination} {source} Change Remove ); }); return (

    Configure Volumes

    {mounts}
    DOCKER FOLDER LOCAL FOLDER
    ); } }); module.exports = ContainerSettingsVolumes; ================================================ FILE: src/components/Containers.react.js ================================================ import $ from 'jquery'; import _ from 'underscore'; import React from 'react'; import Router from 'react-router'; import containerStore from '../stores/ContainerStore'; import ContainerList from './ContainerList.react'; import Header from './Header.react'; import metrics from '../utils/MetricsUtil'; import {shell} from 'electron'; import machine from '../utils/DockerMachineUtil'; var Containers = React.createClass({ contextTypes: { router: React.PropTypes.func }, getInitialState: function () { return { sidebarOffset: 0, containers: containerStore.getState().containers, sorted: this.sorted(containerStore.getState().containers) }; }, componentDidMount: function () { containerStore.listen(this.update); }, componentWillUnmount: function () { containerStore.unlisten(this.update); }, sorted: function (containers) { return _.values(containers).sort(function (a, b) { if (a.Favorite && !b.Favorite) { return -1; } else if (!a.Favorite && b.Favorite) { return 1; } else { if (a.State.Downloading && !b.State.Downloading) { return -1; } else if (!a.State.Downloading && b.State.Downloading) { return 1; } else { if (a.State.Running && !b.State.Running) { return -1; } else if (!a.State.Running && b.State.Running) { return 1; } else { return a.Name.localeCompare(b.Name); } } } }); }, update: function () { let containers = containerStore.getState().containers; let sorted = this.sorted(containerStore.getState().containers); let name = this.context.router.getCurrentParams().name; if (containerStore.getState().pending) { this.context.router.transitionTo('pull'); } else if (name && !containers[name]) { if (sorted.length) { this.context.router.transitionTo('containerHome', {name: sorted[0].Name}); } else { this.context.router.transitionTo('search'); } } this.setState({ containers: containers, sorted: sorted, pending: containerStore.getState().pending }); }, handleScroll: function (e) { if (e.target.scrollTop > 0 && !this.state.sidebarOffset) { this.setState({ sidebarOffset: e.target.scrollTop }); } else if (e.target.scrollTop === 0 && this.state.sidebarOffset) { this.setState({ sidebarOffset: 0 }); } }, handleNewContainer: function () { $(this.getDOMNode()).find('.new-container-item').parent().fadeIn(); this.context.router.transitionTo('search'); metrics.track('Pressed New Container'); }, handleClickPreferences: function () { metrics.track('Opened Preferences', { from: 'app' }); this.context.router.transitionTo('preferences'); }, handleClickDockerTerminal: function () { metrics.track('Opened Docker Terminal', { from: 'app' }); machine.dockerTerminal(); }, handleClickReportIssue: function () { metrics.track('Opened Issue Reporter', { from: 'app' }); shell.openExternal('https://github.com/docker/kitematic'); }, render: function () { var sidebarHeaderClass = 'sidebar-header'; if (this.state.sidebarOffset) { sidebarHeaderClass += ' sep'; } var container = this.context.router.getCurrentParams().name ? this.state.containers[this.context.router.getCurrentParams().name] : {}; return (

    Containers

    New
    DOCKER CLI
    ); } }); module.exports = Containers; ================================================ FILE: src/components/Header.react.js ================================================ import React from 'react/addons'; import RetinaImage from 'react-retina-image'; import util from '../utils/Util'; import metrics from '../utils/MetricsUtil'; import electron from 'electron'; const remote = electron.remote; const Menu = remote.Menu; const MenuItem = remote.MenuItem; import accountStore from '../stores/AccountStore'; import accountActions from '../actions/AccountActions'; import Router from 'react-router'; import classNames from 'classnames'; var Header = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return { fullscreen: false, updateAvailable: false, username: accountStore.getState().username, verified: accountStore.getState().verified }; }, componentDidMount: function () { document.addEventListener('keyup', this.handleDocumentKeyUp, false); accountStore.listen(this.update); }, componentWillUnmount: function () { document.removeEventListener('keyup', this.handleDocumentKeyUp, false); accountStore.unlisten(this.update); }, update: function () { let accountState = accountStore.getState(); this.setState({ username: accountState.username, verified: accountState.verified }); }, handleDocumentKeyUp: function (e) { if (e.keyCode === 27 && remote.getCurrentWindow().isFullScreen()) { remote.getCurrentWindow().setFullScreen(false); this.forceUpdate(); } }, handleClose: function () { if (util.isWindows() || util.isLinux()) { remote.getCurrentWindow().close(); } else { remote.getCurrentWindow().hide(); } }, handleMinimize: function () { remote.getCurrentWindow().minimize(); }, handleFullscreen: function () { if (util.isWindows()) { if (remote.getCurrentWindow().isMaximized()) { remote.getCurrentWindow().unmaximize(); } else { remote.getCurrentWindow().maximize(); } this.setState({ fullscreen: remote.getCurrentWindow().isMaximized() }); } else { remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen()); this.setState({ fullscreen: remote.getCurrentWindow().isFullScreen() }); } }, handleFullscreenHover: function () { this.update(); }, handleUserClick: function (e) { let menu = new Menu(); if (!this.state.verified) { menu.append(new MenuItem({ label: 'I\'ve Verified My Email Address', click: this.handleVerifyClick})); } menu.append(new MenuItem({ label: 'Sign Out', click: this.handleLogoutClick})); menu.popup(remote.getCurrentWindow(), e.currentTarget.offsetLeft, e.currentTarget.offsetTop + e.currentTarget.clientHeight + 10); }, handleLoginClick: function () { this.transitionTo('login'); metrics.track('Opened Log In Screen'); }, handleLogoutClick: function () { metrics.track('Logged Out'); accountActions.logout(); }, handleVerifyClick: function () { metrics.track('Verified Account', { from: 'header' }); accountActions.verify(); }, renderLogo: function () { return (
    ); }, renderWindowButtons: function () { let buttons; if (util.isWindows()) { buttons = (
    ); } else { buttons = (
    ); } return buttons; }, renderDashboardHeader: function () { let headerClasses = classNames({ bordered: !this.props.hideLogin, header: true, 'no-drag': true }); let username; if (this.props.hideLogin) { username = null; } else if (this.state.username) { username = (
    {this.state.username} {this.state.verified ? null : '(Unverified)'}
    ); } else { username = (
    LOGIN
    ); } return (
    {util.isWindows () ? this.renderLogo() : this.renderWindowButtons()} {username}
    {util.isWindows () ? this.renderWindowButtons() : this.renderLogo()}
    ); }, renderBasicHeader: function () { let headerClasses = classNames({ bordered: !this.props.hideLogin, header: true, 'no-drag': true }); return (
    {util.isWindows () ? null : this.renderWindowButtons()}
    {util.isWindows () ? this.renderWindowButtons() : null}
    ); }, render: function () { if (this.props.hideLogin) { return this.renderBasicHeader(); } else { return this.renderDashboardHeader(); } } }); module.exports = Header; ================================================ FILE: src/components/ImageCard.react.js ================================================ import $ from 'jquery'; import React from 'react/addons'; import Router from 'react-router'; import {shell} from 'electron'; import RetinaImage from 'react-retina-image'; import metrics from '../utils/MetricsUtil'; import containerActions from '../actions/ContainerActions'; import imageActions from '../actions/ImageActions'; import containerStore from '../stores/ContainerStore'; import tagStore from '../stores/TagStore'; import tagActions from '../actions/TagActions'; import networkActions from '../actions/NetworkActions'; import networkStore from '../stores/NetworkStore'; import numeral from 'numeral'; import classNames from 'classnames'; var ImageCard = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return { tags: this.props.tags || [], chosenTag: this.props.chosenTag || 'latest', defaultNetwork: this.props.defaultNetwork || 'bridge', networks: networkStore.all(), searchTag: '' }; }, componentDidMount: function () { tagStore.listen(this.updateTags); networkStore.listen(this.updateNetworks); }, componentWillUnmount: function () { tagStore.unlisten(this.updateTags); networkStore.unlisten(this.updateNetworks); }, updateTags: function () { let repo = this.props.image.namespace + '/' + this.props.image.name; let state = tagStore.getState(); if (this.state.tags.length && !state.tags[repo]) { $(this.getDOMNode()).find('.tag-overlay').fadeOut(300); } this.setState({ loading: tagStore.getState().loading[repo] || false, tags: tagStore.getState().tags[repo] || [] }); }, updateNetworks: function () { this.setState({ networks: networkStore.all() }); }, handleTagClick: function (tag) { this.setState({ chosenTag: tag }); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeOut(300); metrics.track('Selected Image Tag'); }, handleNetworkClick: function (network) { this.setState({ defaultNetwork: network }); var $networkOverlay = $(this.getDOMNode()).find('.network-overlay'); $networkOverlay.fadeOut(300); metrics.track('Selected Default Network'); }, handleClick: function () { metrics.track('Created Container', { from: 'search', private: this.props.image.is_private, official: this.props.image.namespace === 'library', userowned: this.props.image.is_user_repo, recommended: this.props.image.is_recommended, local: this.props.image.is_local || false }); let name = containerStore.generateName(this.props.image.name); let localImage = this.props.image.is_local || false; let repo = (this.props.image.namespace === 'library' || this.props.image.namespace === 'local') ? this.props.image.name : this.props.image.namespace + '/' + this.props.image.name; containerActions.run(name, repo, this.state.chosenTag, this.state.defaultNetwork, localImage); this.transitionTo('containerHome', {name}); }, handleMenuOverlayClick: function () { let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); $menuOverlay.fadeIn(300); }, handleCloseMenuOverlay: function () { var $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); $menuOverlay.fadeOut(300); }, handleTagOverlayClick: function () { let $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeIn(300); let localImage = this.props.image.is_local || false; if (localImage) { tagActions.localTags(this.props.image.namespace + '/' + this.props.image.name, this.props.tags); } else { tagActions.tags(this.props.image.namespace + '/' + this.props.image.name); } this.focusSearchTagInput(); }, handleCloseTagOverlay: function () { let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); $menuOverlay.hide(); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeOut(300); }, handleNetworkOverlayClick: function () { let $networkOverlay = $(this.getDOMNode()).find('.network-overlay'); $networkOverlay.fadeIn(300); }, handleCloseNetworkOverlay: function () { let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); $menuOverlay.hide(); var $networkOverlay = $(this.getDOMNode()).find('.network-overlay'); $networkOverlay.fadeOut(300); }, handleDeleteImgClick: function (image) { if (this.state.chosenTag && !this.props.image.inUse) { imageActions.destroy(image.RepoTags[0].split(':')[0] + ':' + this.state.chosenTag); } }, handleRepoClick: function () { var repoUri = 'https://hub.docker.com/'; if (this.props.image.namespace === 'library') { repoUri = repoUri + '_/' + this.props.image.name; } else { repoUri = repoUri + 'r/' + this.props.image.namespace + '/' + this.props.image.name; } shell.openExternal(repoUri); }, searchTag: function(event) { this.setState({ searchTag: event.target.value }); }, focusSearchTagInput: function() { this.refs.searchTagInput.getDOMNode().focus(); }, render: function() { var name; if (this.props.image.namespace === 'library') { name = (
    official
    {this.props.image.name}
    ); } else { name = (
    {this.props.image.namespace}
    {this.props.image.name}
    ); } var description; if (this.props.image.description) { description = this.props.image.description; } else if (this.props.image.short_description) { description = this.props.image.short_description; } else { description = 'No description.'; } var logoStyle = { backgroundColor: this.props.image.gradient_start }; var imgsrc; if (this.props.image.img) { imgsrc = `https://kitematic.com/recommended/${this.props.image.img}`; } else { imgsrc = 'https://kitematic.com/recommended/kitematic_html.png'; } var tags; if (this.state.loading) { tags = ; } else if (this.state.tags.length === 0) { tags =
    No Tags
    ; } else { var tagDisplay = this.state.tags.filter(tag => tag.name.includes(this.state.searchTag)).map((tag) => { let t = ''; if (tag.name) { t = tag.name; } else { t = tag; } let key = t; if (typeof key === 'undefined') { key = this.props.image.name; } if (t === this.state.chosenTag) { return
    {t}
    ; } else { return
    {t}
    ; } }); tags = (
    {tagDisplay}
    ); } let networkDisplay = this.state.networks.map((network) => { let networkName = network.Name; if (networkName === this.state.defaultNetwork) { return
    {networkName}
    ; } else { return
    {networkName}
    ; } }); let networks = (
    {networkDisplay}
    ); var badge = null; if (this.props.image.namespace === 'library') { badge = ( ); } else if (this.props.image.is_private) { badge = ( ); } let create, overlay; if (this.props.image.is_local) { create = (
    {this.state.chosenTag}
    CREATE
    ); overlay = (
    SELECTED TAG: {this.state.chosenTag}
    Delete Tag
    {this.props.image.inUse ?

    To delete, remove all containers
    using the above image

    : null }
    ); } else { let favCount = (this.props.image.star_count < 1000) ? numeral(this.props.image.star_count).value() : numeral(this.props.image.star_count).format('0.0a').toUpperCase(); let pullCount = (this.props.image.pull_count < 1000) ? numeral(this.props.image.pull_count).value() : numeral(this.props.image.pull_count).format('0a').toUpperCase(); create = (
    {favCount} {pullCount}
    CREATE
    ); overlay = (
    SELECTED TAG: {this.state.chosenTag}
    DEFAULT NETWORK: {this.state.defaultNetwork}
    VIEW ON DOCKER HUB
    ); } let searchTagInputStyle = { outline: 'none', width: 'calc(100% - 30px)' }; return (
    {overlay}

    {tags}

    Please select an default network.

    {networks}
    {badge}
    {name}
    {description}
    {create}
    ); } }); module.exports = ImageCard; ================================================ FILE: src/components/Loading.react.js ================================================ import React from 'react/addons'; import Header from './Header.react'; module.exports = React.createClass({ render: function () { return (
    ); } }); ================================================ FILE: src/components/NewContainerSearch.react.js ================================================ import _ from 'underscore'; import React from 'react/addons'; import Router from 'react-router'; import RetinaImage from 'react-retina-image'; import ImageCard from './ImageCard.react'; import Promise from 'bluebird'; import metrics from '../utils/MetricsUtil'; import classNames from 'classnames'; import repositoryActions from '../actions/RepositoryActions'; import repositoryStore from '../stores/RepositoryStore'; import accountStore from '../stores/AccountStore'; import accountActions from '../actions/AccountActions'; import imageActions from '../actions/ImageActions'; import imageStore from '../stores/ImageStore'; var _searchPromise = null; module.exports = React.createClass({ mixins: [Router.Navigation, Router.State], getInitialState: function () { return { query: '', loading: repositoryStore.loading(), repos: repositoryStore.all(), images: imageStore.all(), imagesErr: imageStore.error, username: accountStore.getState().username, verified: accountStore.getState().verified, accountLoading: accountStore.getState().loading, error: repositoryStore.getState().error, currentPage: repositoryStore.getState().currentPage, totalPage: repositoryStore.getState().totalPage, previousPage: repositoryStore.getState().previousPage, nextPage: repositoryStore.getState().nextPage }; }, componentDidMount: function () { this.refs.searchInput.getDOMNode().focus(); repositoryStore.listen(this.update); accountStore.listen(this.updateAccount); imageStore.listen(this.updateImage); repositoryActions.search(); }, componentWillUnmount: function () { if (_searchPromise) { _searchPromise.cancel(); } repositoryStore.unlisten(this.update); accountStore.unlisten(this.updateAccount); }, update: function () { this.setState({ loading: repositoryStore.loading(), repos: repositoryStore.all(), currentPage: repositoryStore.getState().currentPage, totalPage: repositoryStore.getState().totalPage, previousPage: repositoryStore.getState().previousPage, nextPage: repositoryStore.getState().nextPage, error: repositoryStore.getState().error }); }, updateImage: function (imgStore) { this.setState({ images: imgStore.images, error: imgStore.error }); }, updateAccount: function () { this.setState({ username: accountStore.getState().username, verified: accountStore.getState().verified, accountLoading: accountStore.getState().loading }); }, search: function (query, page = 1) { if (_searchPromise) { _searchPromise.cancel(); _searchPromise = null; } let previousPage, nextPage, totalPage = null; // If query remains, retain pagination if (this.state.query === query) { previousPage = (page - 1 < 1) ? 1 : page - 1; nextPage = (page + 1 > this.state.totalPage) ? this.state.totalPage : page + 1; totalPage = this.state.totalPage; } this.setState({ query: query, loading: true, currentPage: page, previousPage: previousPage, nextPage: nextPage, totalPage: totalPage, error: null }); _searchPromise = Promise.delay(200).then(() => { metrics.track('Searched for Images'); _searchPromise = null; repositoryActions.search(query, page); }).catch(Promise.CancellationError, () => {}); }, handleChange: function (e) { let query = e.target.value; if (query === this.state.query) { return; } this.search(query); }, handlePage: function (page) { let query = this.state.query; this.search(query, page); }, handleFilter: function (filter) { this.setState({error: null}); // If we're clicking on the filter again - refresh if (filter === 'userrepos' && this.getQuery().filter === 'userrepos') { repositoryActions.repos(); } if (filter === 'userimages' && this.getQuery().filter === 'userimages') { imageActions.all(); } if (filter === 'recommended' && this.getQuery().filter === 'recommended') { repositoryActions.recommended(); } this.transitionTo('search', {}, {filter: filter}); metrics.track('Filtered Results', { filter: filter }); }, handleCheckVerification: function () { accountActions.verify(); metrics.track('Verified Account', { from: 'search' }); }, render: function () { let filter = this.getQuery().filter || 'all'; let repos = _.values(this.state.repos) .filter(repo => { if (repo.is_recommended || repo.is_user_repo) { return repo.name.toLowerCase().indexOf(this.state.query.toLowerCase()) !== -1 || repo.namespace.toLowerCase().indexOf(this.state.query.toLowerCase()) !== -1; } return true; }) .filter(repo => filter === 'all' || (filter === 'recommended' && repo.is_recommended) || (filter === 'userrepos' && repo.is_user_repo)); let results, paginateResults; let previous = []; let next = []; if (this.state.previousPage) { let previousPage = this.state.currentPage - 7; if (previousPage < 1) { previousPage = 1; } previous.push((
  • )); for (previousPage; previousPage < this.state.currentPage; previousPage++) { previous.push((
  • {previousPage}
  • )); } } if (this.state.nextPage) { let nextPage = this.state.currentPage + 1; for (nextPage; nextPage < this.state.totalPage; nextPage++) { next.push((
  • {nextPage}
  • )); if (nextPage > this.state.currentPage + 7) { break; } } next.push((
  • )); } let current = (
  • {this.state.currentPage} (current)
  • ); paginateResults = (next.length || previous.length) && (this.state.query !== '') ? ( ) : null; let errorMsg = null; if (this.state.error === null || this.state.error.message.indexOf('getaddrinfo ENOTFOUND') !== -1) { errorMsg = 'There was an error contacting Docker Hub.'; } else { errorMsg = this.state.error.message.replace('HTTP code is 409 which indicates error: conflict - ', ''); } if (this.state.error) { results = (

    {errorMsg}

    ); paginateResults = null; } else if (filter === 'userrepos' && !accountStore.getState().username) { results = (

    Log In or Sign Up to access your Docker Hub repositories.

    ); paginateResults = null; } else if (filter === 'userrepos' && !accountStore.getState().verified) { let spinner = this.state.accountLoading ?
    : null; results = (

    Please verify your Docker Hub account email address

    {spinner}
    ); paginateResults = null; } else if (filter === 'userimages') { // filter out dangling images (aka images with no name/tag) let validImages = this.state.images.filter((image) => image.name !== ''); let userImageItems = validImages.map((image, index) => { image.description = null; let tags = image.tags.join('-'); image.star_count = 0; image.is_local = true; const key = `local-${image.name}-${index}`; return ( ); }); let userImageResults = userImageItems.length ? (

    My Images

    {userImageItems}
    ) : (

    Cannot find any local image.

    ); results = ( {userImageResults} ); paginateResults = null; } else if (this.state.loading) { results = (

    Loading Images

    ); } else if (repos.length) { let recommendedItems = repos.filter(repo => repo.is_recommended).map((image, index) => { const key = `rec-${image.name}-${index}`; return (); }); let otherItems = repos.filter(repo => !repo.is_recommended && !repo.is_user_repo).map((image, index) => { const key = `other-${image.name}-${index}`; return (); }); let recommendedResults = recommendedItems.length ? (

    Recommended

    {recommendedItems}
    ) : null; let userRepoItems = repos.filter(repo => repo.is_user_repo).map((image, index) => { const key = `usr-${image.name}-${index}`; return (); }); let userRepoResults = userRepoItems.length ? (

    My Repositories

    {userRepoItems}
    ) : null; let otherResults; if (otherItems.length) { otherResults = (

    Other Repositories

    {otherItems}
    ); } else { otherResults = null; paginateResults = null; } results = (
    {recommendedResults} {userRepoResults} {otherResults}
    ); } else { if (this.state.query.length) { results = (

    Cannot find a matching image.

    ); } else { results = (

    No Images

    ); } } let loadingClasses = classNames({ hidden: !this.state.loading, spinner: true, loading: true, 'la-ball-clip-rotate': true, 'la-dark': true, 'la-sm': true }); let magnifierClasses = classNames({ hidden: this.state.loading, icon: true, 'icon-search': true, 'search-icon': true }); let searchClasses = classNames('search-bar'); if (filter === 'userimages') { searchClasses = classNames('search-bar', { hidden: true }); } return (
    FILTER BY All Recommended My Repos My Images
    {results}
    {paginateResults}
    ); } }); ================================================ FILE: src/components/Preferences.react.js ================================================ import React from 'react/addons'; import metrics from '../utils/MetricsUtil'; import Router from 'react-router'; import util from '../utils/Util'; import electron from 'electron'; const remote = electron.remote; var Preferences = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return { closeVMOnQuit: localStorage.getItem('settings.closeVMOnQuit') === 'true', useVM: localStorage.getItem('settings.useVM') === 'true', metricsEnabled: metrics.enabled(), terminalShell: localStorage.getItem('settings.terminalShell') || "sh", terminalPath: localStorage.getItem('settings.terminalPath') || "/usr/bin/xterm", startLinkedContainers: localStorage.getItem('settings.startLinkedContainers') === 'true' }; }, handleGoBackClick: function () { this.goBack(); metrics.track('Went Back From Preferences'); }, handleChangeCloseVMOnQuit: function (e) { var checked = e.target.checked; this.setState({ closeVMOnQuit: checked }); localStorage.setItem('settings.closeVMOnQuit', checked); metrics.track('Toggled Close VM On Quit', { close: checked }); }, handleChangeUseVM: function (e) { var checked = e.target.checked; this.setState({ useVM: checked }); localStorage.setItem('settings.useVM', checked); util.isNative(); metrics.track('Toggled VM or Native settting', { vm: checked }); }, handleChangeMetricsEnabled: function (e) { var checked = e.target.checked; this.setState({ metricsEnabled: checked }); metrics.setEnabled(checked); metrics.track('Toggled util/MetricsUtil', { enabled: checked }); }, handleChangeTerminalShell: function (e) { var value = e.target.value; this.setState({ terminalShell: value }); localStorage.setItem('settings.terminalShell', value); }, handleChangeTerminalPath: function (e) { var value = e.target.value; this.setState({ terminalPath: value }); localStorage.setItem('settings.terminalPath', value); }, handleChangeStartLinkedContainers: function (e) { var checked = e.target.checked; this.setState({ startLinkedContainers: checked }); localStorage.setItem('settings.startLinkedContainers', checked ? 'true' : 'false'); }, render: function () { var vmSettings, vmShutdown, nativeSetting, linuxSettings; if (process.platform !== 'linux') { // We are on a Mac or Windows if (util.isNative() || (localStorage.getItem('settings.useVM') === 'true')) { nativeSetting = (
    ); } if (!util.isNative()) { vmShutdown = (
    ); } vmSettings = (
    VM Settings
    {vmShutdown} {nativeSetting}
    ); } if (process.platform === "linux") { linuxSettings = (
    ) } return (
    Go Back {vmSettings}
    App Settings
    {linuxSettings}
    ); } }); module.exports = Preferences; ================================================ FILE: src/components/Radial.react.js ================================================ import React from 'react'; import classNames from 'classnames'; var Radial = React.createClass({ render: function () { var percentage; if ((this.props.progress !== null && this.props.progress !== undefined) && !this.props.spin && !this.props.error) { percentage = (
    ); } else { percentage =
    ; } var classes = classNames({ 'radial-progress': true, 'radial-spinner': this.props.spin, 'radial-negative': this.props.error, 'radial-thick': this.props.thick || false, 'radial-gray': this.props.gray || false, 'radial-transparent': this.props.transparent || false }); return (
    {percentage}
    ); } }); module.exports = Radial; ================================================ FILE: src/components/Setup.react.js ================================================ import React from 'react/addons'; import Router from 'react-router'; import Radial from './Radial.react.js'; import RetinaImage from 'react-retina-image'; import Header from './Header.react'; import util from '../utils/Util'; import metrics from '../utils/MetricsUtil'; import setupStore from '../stores/SetupStore'; import setupActions from '../actions/SetupActions'; import {shell} from 'electron'; var Setup = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return setupStore.getState(); }, componentDidMount: function () { setupStore.listen(this.update); }, componentWillUnmount: function () { setupStore.unlisten(this.update); }, update: function () { this.setState(setupStore.getState()); }, handleErrorRetry: function () { setupActions.retry(false); }, handleUseVbox: function () { setupActions.useVbox(); }, handleErrorRemoveRetry: function () { console.log('Deleting VM and trying again.' ); setupActions.retry(true); }, handleResetSettings: function () { metrics.track('Settings reset', { from: 'setup' }); localStorage.removeItem('settings.useVM'); setupActions.retry(false); }, handleToolBox: function () { metrics.track('Getting toolbox', { from: 'setup' }); shell.openExternal('https://www.docker.com/docker-toolbox'); }, handleLinuxDockerInstall: function () { metrics.track('Opening Linux Docker installation instructions', { from: 'setup' }); shell.openExternal('http://docs.docker.com/linux/started/'); }, renderContents: function () { return (
    ); }, renderProgress: function () { let title = 'Starting Docker VM'; let descr = 'To run Docker containers on your computer, Kitematic is starting a Linux virtual machine. This may take a minute...'; if (util.isNative()) { title = 'Checking Docker'; descr = 'To run Docker containers on your computer, Kitematic is checking the Docker connection.'; } return (
    {this.renderContents()}

    {title}

    {descr}

    ); }, renderError: function () { let deleteVmAndRetry; if (util.isLinux()) { if (!this.state.started) { deleteVmAndRetry = ( ); } } else if (util.isNative()) { deleteVmAndRetry = ( ); } else if (this.state.started) { deleteVmAndRetry = ( ); } else { deleteVmAndRetry = ( ); } let usualError = (

    Setup Error

    We're Sorry!

    There seems to have been an unexpected error with Kitematic:

    {this.state.error.message || this.state.error}

    {{deleteVmAndRetry}}

    ); if (util.isNative()) { if (util.isLinux()) { usualError = (

    Setup Initialization

    We couldn't find a native setup - Click the Retry button to check again.

    ); } else { usualError = (

    Setup Initialization

    We couldn't find a native setup - Click the VirtualBox button to use VirtualBox instead or Retry to check again.

    {{deleteVmAndRetry}}

    ); } } return (
    {usualError}
    ); }, render: function () { if (this.state.error) { return this.renderError(); } else { return this.renderProgress(); } } }); module.exports = Setup; ================================================ FILE: src/main.js ================================================ import "./app"; //# sourceMappingURL=main.js.map ================================================ FILE: src/main.ts ================================================ import "./app"; ================================================ FILE: src/menutemplate.js ================================================ import electron from 'electron'; const remote = electron.remote; import {shell} from 'electron'; import router from './router'; import util from './utils/Util'; import metrics from './utils/MetricsUtil'; import machine from './utils/DockerMachineUtil'; import docker from './utils/DockerUtil'; const app = remote.app; const window = remote.getCurrentWindow(); // main.js var MenuTemplate = function () { return [ { label: 'Kitematic', submenu: [ { label: 'About Kitematic', enabled: !!docker.host, click: function () { metrics.track('Opened About', { from: 'menu' }); router.get().transitionTo('about'); if (window.isMinimized()){ window.restore(); } } }, { type: 'separator' }, { label: 'Preferences', accelerator: util.CommandOrCtrl() + '+,', enabled: !!docker.host, click: function () { metrics.track('Opened Preferences', { from: 'menu' }); router.get().transitionTo('preferences'); if (window.isMinimized()){ window.restore(); } } }, { type: 'separator' }, { type: 'separator' }, { label: 'Hide Kitematic', accelerator: util.CommandOrCtrl() + '+H', selector: 'hide:' }, { label: 'Hide Others', accelerator: util.CommandOrCtrl() + '+Alt+H', selector: 'hideOtherApplications:' }, { label: 'Show All', selector: 'unhideAllApplications:' }, { type: 'separator' }, { label: 'Quit', accelerator: util.CommandOrCtrl() + '+Q', click: function() { app.quit(); } } ] }, { label: 'File', submenu: [ { type: 'separator' }, { label: 'Open Docker Command Line Terminal', accelerator: util.CommandOrCtrl() + '+Shift+T', enabled: !!docker.host, click: function() { metrics.track('Opened Docker Terminal', { from: 'menu' }); machine.dockerTerminal(); } } ] }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: util.CommandOrCtrl() + '+Z', selector: 'undo:' }, { label: 'Redo', accelerator: 'Shift+' + util.CommandOrCtrl() + '+Z', selector: 'redo:' }, { type: 'separator' }, { label: 'Cut', accelerator: util.CommandOrCtrl() + '+X', selector: 'cut:' }, { label: 'Copy', accelerator: util.CommandOrCtrl() + '+C', selector: 'copy:' }, { label: 'Paste', accelerator: util.CommandOrCtrl() + '+V', selector: 'paste:' }, { label: 'Select All', accelerator: util.CommandOrCtrl() + '+A', selector: 'selectAll:' } ] }, { label: 'View', submenu: [ { label: 'Refresh Container List', accelerator: util.CommandOrCtrl() + '+R', enabled: !!docker.host, click: function() { metrics.track('Refreshed Container List', { from: 'menu' }); docker.fetchAllContainers(); } }, { label: 'Toggle Chromium Developer Tools', accelerator: 'Alt+' + util.CommandOrCtrl() + '+I', click: function() { remote.getCurrentWindow().toggleDevTools(); } } ] }, { label: 'Window', submenu: [ { label: 'Minimize', accelerator: util.CommandOrCtrl() + '+M', selector: 'performMiniaturize:' }, { label: 'Close', accelerator: util.CommandOrCtrl() + '+W', click: function () { remote.getCurrentWindow().hide(); } }, { type: 'separator' }, { label: 'Bring All to Front', selector: 'arrangeInFront:' }, { type: 'separator' }, { label: 'Kitematic', accelerator: 'Cmd+0', click: function () { remote.getCurrentWindow().show(); } }, ] }, { label: 'Help', submenu: [ { label: 'Report Issue or Suggest Feedback', click: function () { metrics.track('Opened Issue Reporter', { from: 'menu' }); shell.openExternal('https://github.com/kitematic/kitematic/issues/new'); } } ] } ]; }; module.exports = MenuTemplate; ================================================ FILE: src/router.js ================================================ module.exports = { router: null, get: function () { return this.router; }, set: function (router) { this.router = router; } }; ================================================ FILE: src/routes.js ================================================ import React from 'react/addons'; import Setup from './components/Setup.react'; import Account from './components/Account.react'; import AccountSignup from './components/AccountSignup.react'; import AccountLogin from './components/AccountLogin.react'; import Containers from './components/Containers.react'; import ContainerDetails from './components/ContainerDetails.react'; import ContainerHome from './components/ContainerHome.react'; import ContainerSettings from './components/ContainerSettings.react'; import ContainerSettingsGeneral from './components/ContainerSettingsGeneral.react'; import ContainerSettingsPorts from './components/ContainerSettingsPorts.react'; import ContainerSettingsVolumes from './components/ContainerSettingsVolumes.react'; import ContainerSettingsNetwork from './components/ContainerSettingsNetwork.react'; import ContainerSettingsAdvanced from './components/ContainerSettingsAdvanced.react'; import Preferences from './components/Preferences.react'; import About from './components/About.react'; import Loading from './components/Loading.react'; import NewContainerSearch from './components/NewContainerSearch.react'; import Router from 'react-router'; var Route = Router.Route; var DefaultRoute = Router.DefaultRoute; var RouteHandler = Router.RouteHandler; var App = React.createClass({ render: function () { return ( ); } }); var routes = ( ); module.exports = routes; ================================================ FILE: src/stores/AccountStore.js ================================================ import alt from '../alt'; import accountServerActions from '../actions/AccountServerActions'; import accountActions from '../actions/AccountActions'; class AccountStore { constructor () { this.bindActions(accountServerActions); this.bindActions(accountActions); this.prompted = false; this.loading = false; this.errors = {}; this.verified = false; this.username = null; } skip () { this.setState({ prompted: true }); } login () { this.setState({ loading: true, errors: {} }); } logout () { this.setState({ loading: false, errors: {}, username: null, verified: false }); } signup () { this.setState({ loading: true, errors: {} }); } loggedin ({username, verified}) { this.setState({username, verified, errors: {}, loading: false}); } loggedout () { this.setState({ loading: false, errors: {}, username: null, verified: false }); } signedup ({username}) { this.setState({username, errors: {}, loading: false}); } verify () { this.setState({loading: true}); } verified ({verified}) { this.setState({verified, loading: false}); } prompted ({prompted}) { this.setState({prompted}); } errors ({errors}) { this.setState({errors, loading: false}); } } export default alt.createStore(AccountStore); ================================================ FILE: src/stores/ContainerStore.js ================================================ import _ from 'underscore'; import alt from '../alt'; import containerServerActions from '../actions/ContainerServerActions'; import containerActions from '../actions/ContainerActions'; let MAX_LOG_SIZE = 3000; class ContainerStore { constructor () { this.bindActions(containerActions); this.bindActions(containerServerActions); this.containers = {}; // Pending container to create this.pending = null; } error ({name, error}) { let containers = this.containers; if (containers[name]) { containers[name].Error = error; } this.setState({containers}); } start ({name}) { let containers = this.containers; if (containers[name]) { containers[name].State.Starting = true; this.setState({containers}); } } started ({name}) { let containers = this.containers; if (containers[name]) { containers[name].State.Starting = false; containers[name].State.Updating = false; this.setState({containers}); } } stopped ({id}) { let containers = this.containers; let container = _.find(_.values(containers), c => c.Id === id || c.Name === id); if (containers[container.Name]) { containers[container.Name].State.Stopping = false; this.setState({containers}); } } kill ({id}) { let containers = this.containers; let container = _.find(_.values(containers), c => c.Id === id || c.Name === id); if (containers[container.Name]) { containers[container.Name].State.Stopping = true; this.setState({containers}); } } rename ({name, newName}) { let containers = this.containers; let data = containers[name]; data.Name = newName; if (data.State) { data.State.Updating = true; } containers[newName] = data; delete containers[name]; this.setState({containers}); } added ({container}) { let containers = this.containers; containers[container.Name] = container; this.setState({containers}); } update ({name, container}) { let containers = this.containers; if (containers[name] && containers[name].State && containers[name].State.Updating) { return; } if (containers[name].State.Stopping) { return; } _.extend(containers[name], container); if (containers[name].State) { containers[name].State.Updating = true; } this.setState({containers}); } updated ({container}) { if (!container || !container.Name) { return; } let containers = this.containers; if (containers[container.Name] && containers[container.Name].State.Updating) { return; } if (containers[container.Name] && containers[container.Name].Logs) { container.Logs = containers[container.Name].Logs; } containers[container.Name] = container; this.setState({containers}); } allUpdated ({containers}) { this.setState({containers}); } // Receives the name of the container and columns of progression // A column represents progression for one or more layers progress ({name, progress}) { let containers = this.containers; if (containers[name]) { containers[name].Progress = progress; } this.setState({containers}); } destroyed ({id}) { let containers = this.containers; let container = _.find(_.values(containers), c => c.Id === id || c.Name === id); if (container && container.State && container.State.Updating) { return; } if (container) { delete containers[container.Name]; this.setState({containers}); } } waiting ({name, waiting}) { let containers = this.containers; if (containers[name]) { containers[name].State.Waiting = waiting; } this.setState({containers}); } pending ({repo, tag}) { let pending = {repo, tag}; this.setState({pending}); } clearPending () { this.setState({pending: null}); } log ({name, entry}) { let container = this.containers[name]; if (!container) { return; } if (!container.Logs) { container.Logs = []; } container.Logs.push.apply(container.Logs, entry.split('\n').filter(e => e.length)); container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE); this.emitChange(); } logs ({name, logs}) { let container = this.containers[name]; if (!container) { return; } container.Logs = logs.split('\n'); container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE); this.emitChange(); } toggleFavorite ({name}) { let containers = this.containers; if (containers[name]) { containers[name].Favorite = !containers[name].Favorite; } this.setState({containers}); } static generateName (repo) { const base = _.last(repo.split('/')); const names = _.keys(this.getState().containers); var count = 1; var name = base; while (true) { if (names.indexOf(name) === -1) { return name; } else { count++; name = base + '-' + count; } } } } export default alt.createStore(ContainerStore); ================================================ FILE: src/stores/ImageStore.js ================================================ import alt from '../alt'; import imageActions from '../actions/ImageActions'; import imageServerActions from '../actions/ImageServerActions'; class ImageStore { constructor () { this.bindActions(imageActions); this.bindActions(imageServerActions); this.results = []; this.images = []; this.imagesLoading = false; this.resultsLoading = false; this.error = null; } error (error) { this.setState({error: error, imagesLoading: false, resultsLoading: false}); } clearError () { this.setState({error: null}); } destroyed (data) { let images = this.images; if ((data && data[1] && data[1].Deleted)) { delete images[data[1].Deleted]; } this.setState({error: null}); } updated (images) { let tags = {}; let finalImages = []; images.map((image) => { if (image.RepoTags) { image.RepoTags.map(repoTags => { let [name, tag] = repoTags.split(':'); if (typeof tags[name] !== 'undefined') { finalImages[tags[name]].tags.push(tag); if (image.inUse) { finalImages[tags[name]].inUse = image.inUse; } } else { image.tags = [tag]; tags[name] = finalImages.length; finalImages.push(image); } }); } }); this.setState({error: null, images: finalImages, imagesLoading: false}); } static all () { let state = this.getState(); return state.images; } } export default alt.createStore(ImageStore); ================================================ FILE: src/stores/NetworkStore.js ================================================ import alt from '../alt'; import networkActions from '../actions/NetworkActions'; class NetworkStore { constructor () { this.bindActions(networkActions); this.networks = []; this.pending = null; this.error = null; } error (error) { this.setState({error: error}); } updated (networks) { this.setState({error: null, networks: networks}); } pending () { this.setState({pending: true}); } clearPending () { this.setState({pending: null}); } static all () { let state = this.getState(); return state.networks; } } export default alt.createStore(NetworkStore); ================================================ FILE: src/stores/RepositoryStore.js ================================================ import _ from 'underscore'; import alt from '../alt'; import repositoryServerActions from '../actions/RepositoryServerActions'; import repositoryActions from '../actions/RepositoryActions'; import accountServerActions from '../actions/AccountServerActions'; import accountStore from './AccountStore'; class RepositoryStore { constructor () { this.bindActions(repositoryActions); this.bindActions(repositoryServerActions); this.bindActions(accountServerActions); this.results = []; this.recommended = []; this.repos = []; this.query = null; this.nextPage = null; this.previousPage = null; this.currentPage = 1; this.totalPage = null; this.reposLoading = false; this.recommendedLoading = false; this.resultsLoading = false; this.error = null; } error ({error}) { this.setState({error: error, reposLoading: false, recommendedLoading: false, resultsLoading: false}); } repos () { this.setState({reposError: null, reposLoading: true}); } reposLoading () { this.setState({reposLoading: true}); } reposUpdated ({repos}) { let accountState = accountStore.getState(); if (accountState.username && accountState.verified) { this.setState({repos, reposLoading: false}); } else { this.setState({repos: [], reposLoading: false}); } } search ({query, page}) { if (this.query === query) { let previousPage = (page - 1 < 1) ? 1 : page - 1; let nextPage = (page + 1 > this.totalPage) ? this.totalPage : page + 1; this.setState({query: query, error: null, resultsLoading: true, currentPage: page, nextPage: nextPage, previousPage: previousPage}); } else { this.setState({query: query, error: null, resultsLoading: true, nextPage: null, previousPage: null, currentPage: 1, totalPage: null}); } } resultsUpdated ({repos, page, previous, next, total}) { this.setState({results: repos, currentPage: page, previousPage: previous, nextPage: next, totalPage: total, resultsLoading: false}); } recommended () { this.setState({error: null, recommendedLoading: true}); } recommendedUpdated ({repos}) { this.setState({recommended: repos, recommendedLoading: false, error: null}); } loggedout () { this.setState({repos: []}); } static all () { let state = this.getState(); let all = state.recommended.concat(state.repos).concat(state.results); return _.uniq(all, false, repo => repo.namespace + '/' + repo.name); } static loading () { let state = this.getState(); return state.recommendedLoading || state.resultsLoading || state.reposLoading; } } export default alt.createStore(RepositoryStore); ================================================ FILE: src/stores/SetupStore.js ================================================ import alt from '../alt'; import setupServerActions from '../actions/SetupServerActions'; import setupActions from '../actions/SetupActions'; class SetupStore { constructor () { this.bindActions(setupActions); this.bindActions(setupServerActions); this.started = false; this.progress = null; this.error = null; } started ({started}) { this.setState({error: null, started}); } error ({error}) { this.setState({error, progress: null}); } progress ({progress}) { this.setState({progress}); } } export default alt.createStore(SetupStore); ================================================ FILE: src/stores/TagStore.js ================================================ import alt from '../alt'; import tagActions from '../actions/TagActions'; import tagServerActions from '../actions/TagServerActions'; import accountServerActions from '../actions/AccountServerActions'; class TagStore { constructor () { this.bindActions(tagActions); this.bindActions(tagServerActions); this.bindActions(accountServerActions); // maps 'namespace/name' => [list of tags] this.tags = {}; // maps 'namespace/name' => true / false this.loading = {}; } tags ({repo}) { this.loading[repo] = true; this.emitChange(); } localTags ({repo, tags}) { let data = []; tags.map((value) => { data.push({'name': value}); }); this.loading[repo] = true; this.tagsUpdated({repo, tags: data || []}); } tagsUpdated ({repo, tags}) { this.tags[repo] = tags; this.loading[repo] = false; this.emitChange(); } remove ({repo}) { delete this.tags[repo]; delete this.loading[repo]; this.emitChange(); } loggedout () { this.loading = {}; this.tags = {}; this.emitChange(); } error ({repo}) { this.loading[repo] = false; this.emitChange(); } } export default alt.createStore(TagStore); ================================================ FILE: src/utils/ContainerUtil.js ================================================ import _ from 'underscore'; import docker from '../utils/DockerUtil'; var ContainerUtil = { env: function (container) { if (!container || !container.Config || !container.Config.Env) { return []; } return _.map(container.Config.Env, env => { var i = env.indexOf('='); var splits = [env.slice(0, i), env.slice(i + 1)]; return splits; }); }, // Provide Foreground options mode: function (container) { return [ (container && container.Config) ? container.Config.Tty : true, (container && container.Config) ? container.Config.OpenStdin : true, (container && container.HostConfig) ? container.HostConfig.Privileged : false, (container && container.HostConfig) ? container.HostConfig.RestartPolicy : {MaximumRetryCount: 0, Name: 'no'} ]; }, // TODO: inject host here instead of requiring Docker ports: function (container) { if (!container || !container.NetworkSettings) { return {}; } var res = {}; var ip = docker.host; var ports = (container.NetworkSettings.Ports) ? container.NetworkSettings.Ports : ((container.HostConfig.PortBindings) ? container.HostConfig.PortBindings : container.Config.ExposedPorts); _.each(ports, function (value, key) { var [dockerPort, portType] = key.split('/'); var localUrl = null; var port = null; if (value && value.length) { port = value[0].HostPort; } localUrl = (port) ? ip + ':' + port : ip + ':' + ''; res[dockerPort] = { url: localUrl, ip: ip, port: port, portType: portType }; }); return res; }, links: function (container) { if (!container || !container.HostConfig || !container.HostConfig.Links) { return []; } var res = _.map(container.HostConfig.Links, (link, key) => { return { "container": link.split(":")[0].split("/")[1], "alias": link.split(":")[1].split("/")[2], } }); return res; }, normalizeLinksPath: function (container, links) { var res = _.map(links, (link) => { return "/"+link.container+":/"+container.Name+"/"+link.alias; }); return res; } }; module.exports = ContainerUtil; ================================================ FILE: src/utils/DockerMachineUtil.js ================================================ import _ from 'underscore'; import path from 'path'; import Promise from 'bluebird'; import fs from 'fs'; import util from './Util'; import child_process from 'child_process'; import which from 'which'; var DockerMachine = { command: function () { if (util.isWindows()) { if (process.env.DOCKER_TOOLBOX_INSTALL_PATH) { return path.join(process.env.DOCKER_TOOLBOX_INSTALL_PATH, 'docker-machine.exe'); } } try { return which.sync('docker-machine'); } catch (ex) { return null; } }, name: function () { return 'default'; }, installed: function () { try { fs.accessSync(this.command(), fs.X_OK); return true; } catch (ex) { return false; } }, version: function () { return util.execFile([this.command(), '-v']).then(stdout => { try { var matchlist = stdout.match(/(\d+\.\d+\.\d+).*/); if (!matchlist || matchlist.length < 2) { return Promise.reject('docker-machine -v output format not recognized.'); } return Promise.resolve(matchlist[1]); } catch (err) { return Promise.resolve(null); } }).catch(() => { return Promise.resolve(null); }); }, isoversion: function (machineName = this.name()) { try { var data = fs.readFileSync(path.join(util.home(), '.docker', 'machine', 'machines', machineName, 'boot2docker.iso'), 'utf8'); var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/); if (match) { return match[1]; } else { return null; } } catch (err) { return null; } }, exists: function (machineName = this.name()) { return this.status(machineName).then(() => { return true; }).catch(() => { return false; }); }, create: function (machineName = this.name()) { return util.execFile([this.command(), '-D', 'create', '-d', 'virtualbox', '--virtualbox-memory', '2048', machineName]); }, start: function (machineName = this.name()) { return util.execFile([this.command(), '-D', 'start', machineName]); }, stop: function (machineName = this.name()) { return util.execFile([this.command(), 'stop', machineName]); }, upgrade: function (machineName = this.name()) { return util.execFile([this.command(), 'upgrade', machineName]); }, rm: function (machineName = this.name()) { return util.execFile([this.command(), 'rm', '-f', machineName]); }, ip: function (machineName = this.name()) { return util.execFile([this.command(), 'ip', machineName]).then(stdout => { return Promise.resolve(stdout.trim().replace('\n', '')); }); }, url: function (machineName = this.name()) { return util.execFile([this.command(), 'url', machineName]).then(stdout => { return Promise.resolve(stdout.trim().replace('\n', '')); }); }, regenerateCerts: function (machineName = this.name()) { return util.execFile([this.command(), 'tls-regenerate-certs', '-f', machineName]); }, status: function (machineName = this.name()) { return new Promise((resolve, reject) => { child_process.execFile(this.command(), ['status', machineName], (error, stdout, stderr) => { if (error) { reject(new Error('Encountered an error: ' + error)); } else { resolve(stdout.trim() + stderr.trim()); } }); }); }, disk: function (machineName = this.name()) { return util.execFile([this.command(), 'ssh', machineName, 'df']).then(stdout => { try { var lines = stdout.split('\n'); var dataline = _.find(lines, function (line) { return line.indexOf('/dev/sda1') !== -1; }); var tokens = dataline.split(' '); tokens = tokens.filter(function (token) { return token !== ''; }); var usedGb = parseInt(tokens[2], 10) / 1000000; var totalGb = parseInt(tokens[3], 10) / 1000000; var percent = parseInt(tokens[4].replace('%', ''), 10); return { used_gb: usedGb.toFixed(2), total_gb: totalGb.toFixed(2), percent: percent }; } catch (err) { return Promise.reject(err); } }); }, memory: function (machineName = this.name()) { return util.execFile([this.command(), 'ssh', machineName, 'free -m']).then(stdout => { try { var lines = stdout.split('\n'); var dataline = _.find(lines, function (line) { return line.indexOf('-/+ buffers') !== -1; }); var tokens = dataline.split(' '); tokens = tokens.filter((token) => { return token !== ''; }); var usedGb = parseInt(tokens[2], 10) / 1000; var freeGb = parseInt(tokens[3], 10) / 1000; var totalGb = usedGb + freeGb; var percent = Math.round(usedGb / totalGb * 100); return { used_gb: usedGb.toFixed(2), total_gb: totalGb.toFixed(2), free_gb: freeGb.toFixed(2), percent: percent }; } catch (err) { return Promise.reject(err); } }); }, dockerTerminal: function (cmd, machineName = this.name()) { cmd = cmd || process.env.SHELL || ''; if (util.isWindows()) { if (util.isNative()) { util.exec('start powershell.exe ' + cmd); } else { this.url(machineName).then(machineUrl => { util.exec('start powershell.exe ' + cmd, {env: { 'DOCKER_HOST': machineUrl, 'DOCKER_CERT_PATH': process.env.DOCKER_CERT_PATH || path.join(util.home(), '.docker', 'machine', 'machines', machineName), 'DOCKER_TLS_VERIFY': 1 } }); }); } } else { var terminal = util.isLinux() ? util.linuxTerminal() : [path.join(process.env.RESOURCES_PATH, 'terminal')]; if (util.isNative()) { terminal.push(cmd); util.execFile(terminal).then(() => {}); } else { this.url(machineName).then(machineUrl => { terminal.push(`DOCKER_HOST=${machineUrl} DOCKER_CERT_PATH=${process.env.DOCKER_CERT_PATH || path.join(util.home(), '.docker/machine/machines/' + machineName)} DOCKER_TLS_VERIFY=1`); terminal.push(cmd); util.execFile(terminal).then(() => {}); }); } } }, virtualBoxLogs: function (machineName = this.name()) { var logsPath = null; if (process.env.MACHINE_STORAGE_PATH) { logsPath = path.join(process.env.MACHINE_STORAGE_PATH, 'machines', machineName, machineName, 'Logs', 'VBox.log'); } else { logsPath = path.join(util.home(), '.docker', 'machine', 'machines', machineName, machineName, 'Logs', 'VBox.log'); } let logData = null; try { logData = fs.readFileSync(logsPath, 'utf8'); } catch (e) { console.error(e); } return logData; } }; module.exports = DockerMachine; ================================================ FILE: src/utils/DockerUtil.js ================================================ import async from 'async'; import fs from 'fs'; import path from 'path'; import dockerode from 'dockerode'; import _ from 'underscore'; import http from 'http'; import child_process from 'child_process'; import util from './Util'; import hubUtil from './HubUtil'; import metrics from '../utils/MetricsUtil'; import containerServerActions from '../actions/ContainerServerActions'; import imageServerActions from '../actions/ImageServerActions'; import networkActions from '../actions/NetworkActions'; import networkStore from '../stores/NetworkStore'; import Promise from 'bluebird'; import rimraf from 'rimraf'; const parseData = (item) => { try { return JSON.parse(item); } catch (err) { return null; } }; const getPullingData = (raw) => raw.split('\n') .filter((item) => item.length > 0) .map(parseData) .filter((item) => !!item); var DockerUtil = { host: null, client: null, placeholders: {}, stream: null, eventStream: null, activeContainerName: null, localImages: null, imagesUsed: [], socketPath: util.isWindows() ? '//./pipe/docker_engine' : '/var/run/docker.sock', setup (ip, name) { if (!ip && !name) { throw new Error('Falsy ip or name passed to docker client setup'); } this.host = ip; if (ip.indexOf('local') !== -1) { try { this.client = new dockerode({socketPath: this.socketPath, headers: {'user-agent': 'kitematic'}}); } catch (error) { throw new Error('Cannot connect to the Docker daemon. Is the daemon running?'); } } else { let certDir = process.env.DOCKER_CERT_PATH || path.join(util.home(), '.docker/machine/machines/', name); if (!fs.existsSync(certDir)) { throw new Error('Certificate directory does not exist'); } this.client = new dockerode({ protocol: 'https', host: ip, port: 2376, ca: fs.readFileSync(path.join(certDir, 'ca.pem')), cert: fs.readFileSync(path.join(certDir, 'cert.pem')), key: fs.readFileSync(path.join(certDir, 'key.pem')), headers: {'user-agent': 'kitematic'}, }); } }, async version () { let version = null; let maxRetries = 10; let retries = 0; let error_message = ""; while (version == null && retries < maxRetries) { this.client.version((error,data) => { if (!error) { version = data.Version; } else { error_message = error; } retries++; }); await Promise.delay(500); } if (version == null) { throw new Error(error_message); } return version; }, init () { this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {}; this.refresh(); this.listen(); this.fetchAllNetworks(); // Resume pulling containers that were previously being pulled _.each(_.values(this.placeholders), container => { containerServerActions.added({container}); this.client.pull(container.Config.Image, (error, stream) => { if (error) { containerServerActions.error({name: container.Name, error}); return; } stream.setEncoding('utf8'); stream.on('data', function () {}); stream.on('end', () => { if (!this.placeholders[container.Name]) { return; } delete this.placeholders[container.Name]; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); this.createContainer(container.Name, {Image: container.Config.Image}); }); }); }); }, isDockerRunning () { try { child_process.execSync('ps ax | grep "docker daemon" | grep -v grep'); } catch (error) { throw new Error('Cannot connect to the Docker daemon. The daemon is not running.'); } }, startContainer (name) { let container = this.client.getContainer(name); container.start((error) => { if (error) { containerServerActions.error({name, error}); console.log('error starting: %o - %o', name, error); return; } containerServerActions.started({name, error}); this.fetchContainer(name); }); }, createContainer (name, containerData) { containerData.name = containerData.Name || name; if (containerData.Config && containerData.Config.Image) { containerData.Image = containerData.Config.Image; } if (containerData.Config && containerData.Config.Hostname) { containerData.Hostname = containerData.Config.Hostname; } if (!containerData.Env && containerData.Config && containerData.Config.Env) { containerData.Env = containerData.Config.Env; } containerData.Volumes = _.mapObject(containerData.Volumes, () => {}); this.client.getImage(containerData.Image).inspect((error, image) => { if (error) { containerServerActions.error({name, error}); return; } if (!containerData.HostConfig || (containerData.HostConfig && !containerData.HostConfig.PortBindings)) { if (!containerData.HostConfig) { containerData.HostConfig = {}; } containerData.HostConfig.PublishAllPorts = true; } let networks = []; if (!_.has(containerData, 'NetworkingConfig') && _.has(containerData.NetworkSettings, 'Networks')) { let EndpointsConfig = {}; networks = _.keys(containerData.NetworkSettings.Networks); if (networks.length) { let networkName = networks.shift(); EndpointsConfig[networkName] = _.extend(containerData.NetworkSettings.Networks[networkName], {Aliases: []}); } containerData.NetworkingConfig = { EndpointsConfig }; } if (image.Config.Cmd) { containerData.Cmd = image.Config.Cmd; } else if (!image.Config.Entrypoint) { containerData.Cmd = 'sh'; } // Keep current config for new container if no changes _.extend(containerData, _.omit(containerData.Config, Object.keys(containerData))); let existing = this.client.getContainer(name); existing.kill(() => { existing.remove(() => { this.client.createContainer(containerData, (error) => { if (error) { console.error(err); containerServerActions.error({name, error}); return; } metrics.track('Container Finished Creating'); this.addOrRemoveNetworks(name, networks, true).finally(() => { this.startContainer(name); delete this.placeholders[name]; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); this.refresh(); }); }); }); }); }); }, fetchContainer (id) { this.client.getContainer(id).inspect((error, container) => { if (error) { containerServerActions.error({name: id, error}); } else { container.Name = container.Name.replace('/', ''); this.client.getImage(container.Image).inspect((error, image) => { if (error) { containerServerActions.error({name, error}); return; } container.InitialPorts = image.Config.ExposedPorts; }); containerServerActions.updated({container}); networkActions.clearPending(); } }); }, fetchAllContainers () { this.client.listContainers({all: true}, (err, containers) => { if (err) { console.error(err); return; } this.imagesUsed = []; async.map(containers, (container, callback) => { this.client.getContainer(container.Id).inspect((error, container) => { if (error) { callback(null, null); return; } let imgSha = container.Image.replace('sha256:', ''); if (_.indexOf(this.imagesUsed, imgSha) === -1) { this.imagesUsed.push(imgSha); } container.Name = container.Name.replace('/', ''); this.client.getImage(container.Image).inspect((error, image) => { if (error) { containerServerActions.error({name, error}); return; } container.InitialPorts = image.Config.ExposedPorts; }); callback(null, container); }); }, (err, containers) => { containers = containers.filter(c => c !== null); if (err) { // TODO: add a global error handler for this console.error(err); return; } containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')}); let favorites = JSON.parse(localStorage.getItem('containers.favorites')) || []; favorites.forEach(name => containerServerActions.toggleFavorite({name})); this.logs(); this.fetchAllImages(); }); }); }, fetchAllImages () { this.client.listImages((err, list) => { if (err) { imageServerActions.error(err); } else { list.map((image, idx) => { let imgSha = image.Id.replace('sha256:', ''); if (_.indexOf(this.imagesUsed, imgSha) !== -1) { list[idx].inUse = true; } else { list[idx].inUse = false; } let imageSplit = ''; if (image.RepoTags) { imageSplit = image.RepoTags[0].split(':'); } else { imageSplit = image.RepoDigests[0].split('@'); } let repo = imageSplit[0]; if(imageSplit.length > 2) { repo = imageSplit[0]+':'+imageSplit[1]; } if (repo.indexOf('/') === -1) { repo = 'local/' + repo; } let repoSplit = repo.split('/'); list[idx].namespace = repoSplit.shift(); list[idx].name = repoSplit.join('/'); }); this.localImages = list; imageServerActions.updated(list); } }); }, fetchAllNetworks () { this.client.listNetworks((err, networks) => { if (err) { networkActions.error(err) } else { networks = networks.sort((n1, n2) => { if (n1.Name > n2.Name) { return 1; } if (n1.Name < n2.Name) { return -1; } return 0; }); networkActions.updated(networks); } }); }, updateContainerNetworks(name, connectedNetworks, disconnectedNetworks) { networkActions.pending(); let disconnectedPromise = this.addOrRemoveNetworks(name, disconnectedNetworks, false); disconnectedPromise.then(() => { let connectedPromise = this.addOrRemoveNetworks(name, connectedNetworks, true); connectedPromise.finally(() => { this.fetchContainer(name); }) }).catch(() => { this.fetchContainer(name); }); }, addOrRemoveNetworks(name, networks, connect) { let promises = _.map(networks, networkName => { let network = this.client.getNetwork(networkName); let operation = (connect === true ? network.connect : network.disconnect).bind(network); return new Promise(function (resolve, reject) { operation({ Container: name }, (err, data) => { if (err) { console.log(err); reject(err); } else { resolve(data); } }); }); }); return Promise.all(promises); }, removeImage (selectedRepoTag) { // Prune all dangling image first this.localImages.some((image) => { if (image.namespace == "" && image.name == "") { return false } if (image.RepoTags) { image.RepoTags.map(repoTag => { if (repoTag === selectedRepoTag) { this.client.getImage(selectedRepoTag).remove({'force': true}, (err, data) => { if (err) { console.error(err); imageServerActions.error(err); } else { imageServerActions.destroyed(data); this.refresh(); } }); return true; } }); } }); }, run (name, repository, tag, network, local = false) { tag = tag || 'latest'; let imageName = repository + ':' + tag; let placeholderData = { Id: util.randomId(), Name: name, Image: imageName, Config: { Image: imageName }, Tty: true, OpenStdin: true, State: { Downloading: true } }; containerServerActions.added({container: placeholderData}); this.placeholders[name] = placeholderData; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); let containerData = { Image: imageName, Tty: true, OpenStdin: true, NetworkingConfig: { EndpointsConfig: { [network]: {} } } }; if (local) { this.createContainer(name, containerData); } else { this.pullImage(repository, tag, error => { if (error) { containerServerActions.error({name, error}); this.refresh(); return; } if (!this.placeholders[name]) { return; } this.createContainer(name, containerData); }, // progress is actually the progression PER LAYER (combined in columns) // not total because it's not accurate enough progress => { containerServerActions.progress({name, progress}); }, () => { containerServerActions.waiting({name, waiting: true}); }); } }, updateContainer (name, data) { let existing = this.client.getContainer(name); existing.inspect((error, existingData) => { if (error) { containerServerActions.error({name, error}); this.refresh(); return; } if (existingData.Config && existingData.Config.Image) { existingData.Image = existingData.Config.Image; } if (!existingData.Env && existingData.Config && existingData.Config.Env) { existingData.Env = existingData.Config.Env; } if ((!existingData.Tty || !existingData.OpenStdin) && existingData.Config && (existingData.Config.Tty || existingData.Config.OpenStdin)) { existingData.Tty = existingData.Config.Tty; existingData.OpenStdin = existingData.Config.OpenStdin; } data.Mounts = data.Mounts || existingData.Mounts; var fullData = _.extend(existingData, data); this.createContainer(name, fullData); }); }, rename (name, newName) { this.client.getContainer(name).rename({name: newName}, error => { if (error && error.statusCode !== 204) { containerServerActions.error({name, error}); return; } var oldPath = util.windowsToLinuxPath(path.join(util.home(), util.documents(), 'Kitematic', name)); var newPath = util.windowsToLinuxPath(path.join(util.home(), util.documents(), 'Kitematic', newName)); this.client.getContainer(newName).inspect((error, container) => { if (error) { // TODO: handle error containerServerActions.error({newName, error}); this.refresh(); } rimraf(newPath, () => { if (fs.existsSync(oldPath)) { fs.renameSync(oldPath, newPath); } container.Mounts.forEach(m => { m.Source = m.Source.replace(oldPath, newPath); }); this.updateContainer(newName, {Mounts: container.Mounts}); rimraf(oldPath, () => {}); }); }); }); }, restart (name) { this.client.getContainer(name).stop({t: 5}, stopError => { if (stopError && stopError.statusCode !== 304) { containerServerActions.error({name, stopError}); this.refresh(); return; } this.client.getContainer(name).start(startError => { if (startError && startError.statusCode !== 304) { containerServerActions.error({name, startError}); this.refresh(); return; } this.fetchContainer(name); }); }); }, stop (name) { this.client.getContainer(name).stop({t: 5}, error => { if (error && error.statusCode !== 304) { containerServerActions.error({name, error}); this.refresh(); return; } this.fetchContainer(name); }); }, start (name, callback) { var self = this; this.client.getContainer(name).inspect((error, container) => { if (error) { containerServerActions.error({name: name, error}); if(callback) callback(error); } else { if(container.HostConfig && container.HostConfig.Links && container.HostConfig.Links.length > 0 && localStorage.getItem('settings.startLinkedContainers') === 'true' ){ self.startLinkedContainers(name, function(error){ if(error){ containerServerActions.error({name: name, error}); if(callback) callback(error); }else{ self.client.getContainer(name).start(error => { if (error && error.statusCode !== 304) { containerServerActions.error({name, error}); this.refresh(); return; }else{ self.fetchContainer(name); if(callback) callback(null); } }); } }) }else{ self.client.getContainer(name).start(error => { if (error && error.statusCode !== 304) { containerServerActions.error({name, error}); this.refresh(); return; }else{ self.fetchContainer(name); if(callback) callback(null); } }); } } }) }, startLinkedContainers (name, callback){ var self = this; this.client.getContainer(name).inspect((error, container) => { if (error) { containerServerActions.error({name: name, error}); if(callback) callback(error); } else { var links = _.map(container.HostConfig.Links, (link, key) => { return link.split(":")[0].split("/")[1]; }); async.map(links, function(link, cb){ var linkedContainer = self.client.getContainer(link); if(linkedContainer){ linkedContainer.inspect((error, linkedContainerInfo) => { if (error) { containerServerActions.error({name: name, error}); cb(error); } else { if(linkedContainerInfo.State.Stopping || linkedContainerInfo.State.Downloading || linkedContainerInfo.State.ExitCode || !linkedContainerInfo.State.Running || linkedContainerInfo.State.Updating ){ self.start(linkedContainerInfo.Id, function(error){ if (error) { containerServerActions.error({name: name, error}); cb(error); }else{ self.fetchContainer(linkedContainerInfo.Id); cb(null); } }); }else{ cb(null); } } }); }else{ cb("linked container "+link+" not found"); } }, function(error, containers) { if(error){ containerServerActions.error({name, error}); if(callback) callback(error); return; }else{ if(callback) callback(null); return; } }); } }); }, destroy (name) { if (this.placeholders[name]) { containerServerActions.destroyed({id: name}); delete this.placeholders[name]; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); this.refresh(); return; } let container = this.client.getContainer(name); container.unpause( () => { container.kill( () => { container.remove( (error) => { if (error) { containerServerActions.error({name, error}); this.refresh(); return; } containerServerActions.destroyed({id: name}); var volumePath = path.join(util.home(), 'Kitematic', name); if (fs.existsSync(volumePath)) { rimraf(volumePath, () => {}); } this.refresh(); }); }); }); }, active (name) { this.detachLog(); this.activeContainerName = name; if (name) { this.logs(); } }, logs () { if (!this.activeContainerName) { return; } this.client.getContainer(this.activeContainerName).logs({ stdout: true, stderr: true, tail: 1000, follow: false, timestamps: 1 }, (err, logBuffer) => { if (err) { // socket hang up can be captured console.error(err); containerServerActions.error({name: this.activeContainerName, err}); return; } let logs = logBuffer.toString(); containerServerActions.logs({name: this.activeContainerName, logs}); this.attach(); }); }, attach () { if (!this.activeContainerName) { return; } this.client.getContainer(this.activeContainerName).logs({ stdout: true, stderr: true, tail: 0, follow: true, timestamps: 1 }, (err, logStream) => { if (err) { // Socket hang up also can be found here console.error(err); return; } this.detachLog() this.stream = logStream; let timeout = null; let batch = ''; logStream.setEncoding('utf8'); logStream.on('data', (chunk) => { batch += chunk; if (!timeout) { timeout = setTimeout(() => { containerServerActions.log({name: this.activeContainerName, entry: batch}); timeout = null; batch = ''; }, 16); } }); }); }, detachLog() { if (this.stream) { this.stream.destroy(); this.stream = null; } }, detachEvent() { if (this.eventStream) { this.eventStream.destroy(); this.eventStream = null; } }, listen () { this.detachEvent() this.client.getEvents((error, stream) => { if (error || !stream) { // TODO: Add app-wide error handler return; } // TODO: Add health-check for existing connection stream.setEncoding('utf8'); stream.on('data', json => { let data = JSON.parse(json); if (['pull', 'untag', 'tag', 'delete', 'attach'].includes(data.status)) { this.refresh(); } if (data.status === 'destroy') { containerServerActions.destroyed({id: data.id}); this.detachLog() } else if (data.status === 'kill') { containerServerActions.kill({id: data.id}); this.detachLog() } else if (data.status === 'stop') { containerServerActions.stopped({id: data.id}); this.detachLog() } else if (data.status === 'create') { this.logs(); this.fetchContainer(data.id); } else if (data.status === 'start') { this.attach(); this.fetchContainer(data.id); } else if (data.id) { this.fetchContainer(data.id); } if (data.Type === 'network') { let action = data.Action; if (action === 'connect' || action === 'disconnect') { // do not fetch container while networks updating via Kitematic if (!networkStore.getState().pending) { this.fetchContainer(data.Actor.Attributes.container); } } else if (action === 'create' || action === 'destroy') { this.fetchAllNetworks(); } } }); this.eventStream = stream; }); }, pullImage (repository, tag, callback, progressCallback, blockedCallback) { const options = { socketPath: this.socketPath, path: '/images/create?fromImage=' + encodeURIComponent(repository) + '&tag=' + tag, method: 'POST', headers: { 'Content-Type': 'application/json', }, }; let config = hubUtil.config(); if (hubUtil.config()) { let [username, password] = hubUtil.creds(config); options.headers['X-Registry-Auth'] = new Buffer(JSON.stringify({username, password})).toString('base64'); } const req = http.request(options, (res) => { res.setEncoding('utf8'); // scheduled to inform about progression at given interval let tick = null; const layerProgress = {}; // Split the loading in a few columns for more feedback let columns = {}; columns.amount = 4; // arbitrary columns.toFill = 0; // the current column index, waiting for layer IDs to be displayed let error = null; res.on('data', (rawData) => { const items = getPullingData(rawData); items.forEach((data) => { if (data.error) { error = data.error; return; } if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) { blockedCallback(); return; } if (data.id && !layerProgress[data.id]) { layerProgress[data.id] = { current: 0, total: 1 }; } if (data.status === 'Downloading') { if (!columns.progress) { columns.progress = []; // layerIDs, nbLayers, maxLayers, progress value let layersToLoad = _.keys(layerProgress).length; let layersPerColumn = Math.floor(layersToLoad / columns.amount); let leftOverLayers = layersToLoad % columns.amount; for (let i = 0; i < columns.amount; i++) { let layerAmount = layersPerColumn; if (i < leftOverLayers) { layerAmount += 1; } columns.progress[i] = {layerIDs: [], nbLayers: 0, maxLayers: layerAmount, value: 0.0}; } } layerProgress[data.id].current = data.progressDetail.current; layerProgress[data.id].total = data.progressDetail.total; // Assign to a column if not done yet if (!layerProgress[data.id].column) { // test if we can still add layers to that column if (columns.progress[columns.toFill].nbLayers === columns.progress[columns.toFill].maxLayers && columns.toFill < columns.amount - 1) { columns.toFill++; } layerProgress[data.id].column = columns.toFill; columns.progress[columns.toFill].layerIDs.push(data.id); columns.progress[columns.toFill].nbLayers++; } if (!tick) { tick = setTimeout(() => { clearInterval(tick); tick = null; for (let i = 0; i < columns.amount; i++) { columns.progress[i].value = 0.0; if (columns.progress[i].nbLayers > 0) { let layer; let totalSum = 0; let currentSum = 0; for (let j = 0; j < columns.progress[i].nbLayers; j++) { layer = layerProgress[columns.progress[i].layerIDs[j]]; totalSum += layer.total; currentSum += layer.current; } if (totalSum > 0) { columns.progress[i].value = Math.min(100.0 * currentSum / totalSum, 100); } else { columns.progress[i].value = 0.0; } } } progressCallback(columns); }, 16); } } }); }); res.on('end', () => callback(error)); }); req.on('error', (err) => { error = err; }); req.end(); }, refresh () { this.fetchAllContainers(); } }; module.exports = DockerUtil; ================================================ FILE: src/utils/HubUtil.js ================================================ import _ from 'underscore'; import request from 'request'; import accountServerActions from '../actions/AccountServerActions'; import metrics from './MetricsUtil'; let HUB2_ENDPOINT = process.env.HUB2_ENDPOINT || 'https://hub.docker.com/v2'; module.exports = { init: function () { accountServerActions.prompted({prompted: localStorage.getItem('auth.prompted')}); let username = localStorage.getItem('auth.username'); let verified = localStorage.getItem('auth.verified') === 'true'; if (username) { accountServerActions.loggedin({username, verified}); } }, username: function () { return localStorage.getItem('auth.username') || null; }, // Returns the base64 encoded index token or null if no token exists config: function () { let config = localStorage.getItem('auth.config'); if (!config) { return null; } return config; }, // Retrives the current jwt hub token or null if no token exists jwt: function () { let jwt = localStorage.getItem('auth.jwt'); if (!jwt) { return null; } return jwt; }, prompted: function () { return localStorage.getItem('auth.prompted'); }, setPrompted: function (prompted) { localStorage.setItem('auth.prompted', true); accountServerActions.prompted({prompted}); }, request: function (req, callback) { let jwt = this.jwt(); if (jwt) { _.extend(req, { headers: { Authorization: `JWT ${jwt}` } }); } // First attempt with existing JWT request(req, (error, response, body) => { let data; try { data = JSON.parse(body); } catch (e) { console.error('Json parse error: %o', e); } // If the JWT has expired, then log in again to get a new JWT if (data && data.detail && data.detail.indexOf('expired') !== -1) { let config = this.config(); if (!this.config()) { this.logout(); return; } let [username, password] = this.creds(config); this.auth(username, password, (error, response, body) => { let data = JSON.parse(body); if (response.statusCode === 200 && data && data.token) { localStorage.setItem('auth.jwt', data.token); this.request(req, callback); } else { this.logout(); } }); } else { callback(error, response, body); } }); }, loggedin: function () { return this.jwt() && this.config(); }, logout: function () { accountServerActions.loggedout(); localStorage.removeItem('auth.jwt'); localStorage.removeItem('auth.username'); localStorage.removeItem('auth.verified'); localStorage.removeItem('auth.config'); this.request({ url: `${HUB2_ENDPOINT}/logout` }, (error, response, body) => {}); }, login: function (username, password, callback) { this.auth(username, password, (error, response, body) => { if (error) { accountServerActions.errors({errors: {detail: error.message}}); callback(error); return; } let data = JSON.parse(body); if (response.statusCode === 200) { if (data.token) { localStorage.setItem('auth.jwt', data.token); localStorage.setItem('auth.username', username); localStorage.setItem('auth.verified', true); localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64')); accountServerActions.loggedin({username, verified: true}); accountServerActions.prompted({prompted: true}); metrics.track('Successfully Logged In'); if (callback) { callback(); } require('./RegHubUtil').repos(); } else { accountServerActions.errors({errors: {detail: 'Did not receive login token.'}}); if (callback) { callback(new Error('Did not receive login token.')); } } } else if (response.statusCode === 401) { if (data && data.detail && data.detail.indexOf('Account not active yet') !== -1) { accountServerActions.loggedin({username, verified: false}); accountServerActions.prompted({prompted: true}); localStorage.setItem('auth.username', username); localStorage.setItem('auth.verified', false); localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64')); if (callback) { callback(); } } else { accountServerActions.errors({errors: data}); if (callback) { callback(new Error(data.detail)); } } } }); }, auth: function (username, password, callback) { request.post(`${HUB2_ENDPOINT}/users/login/`, {form: {username, password}}, (error, response, body) => { callback(error, response, body); }); }, verify: function () { let config = this.config(); if (!config) { this.logout(); return; } let [username, password] = this.creds(config); this.login(username, password); }, creds: function (config) { return new Buffer(config, 'base64').toString().split(/:(.+)?/).slice(0, 2); }, // Signs up and places a token under ~/.dockercfg and saves a jwt to localstore signup: function (username, password, email, subscribe) { request.post(`${HUB2_ENDPOINT}/users/signup/`, { form: { username, password, email, subscribe } }, (err, response, body) => { if (response && response.statusCode === 204) { accountServerActions.signedup({username, verified: false}); accountServerActions.prompted({prompted: true}); localStorage.setItem('auth.username', username); localStorage.setItem('auth.verified', false); localStorage.setItem('auth.config', new Buffer(username + ':' + password).toString('base64')); metrics.track('Successfully Signed Up'); } else { let data = JSON.parse(body); let errors = {}; for (let key in data) { errors[key] = data[key][0]; } accountServerActions.errors({errors}); } }); }, }; ================================================ FILE: src/utils/MetricsUtil.js ================================================ import assign from 'object-assign'; import Mixpanel from 'mixpanel'; import uuid from 'node-uuid'; import fs from 'fs'; import path from 'path'; import util from './Util'; import os from 'os'; import osxRelease from 'osx-release'; var settings; try { settings = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) { settings = {}; } var token = process.env.NODE_ENV === 'development' ? settings['mixpanel-dev'] : settings.mixpanel; if (!token) { token = 'none'; } var mixpanel = Mixpanel.init(token); if (localStorage.getItem('metrics.enabled') === null) { localStorage.setItem('metrics.enabled', true); } var Metrics = { enabled: function () { return localStorage.getItem('metrics.enabled') === 'true'; }, setEnabled: function (enabled) { localStorage.setItem('metrics.enabled', !!enabled); }, track: function (name, data) { data = data || {}; if (!name) { return; } if (localStorage.getItem('metrics.enabled') !== 'true') { return; } let id = localStorage.getItem('metrics.id'); if (!id) { id = uuid.v4(); localStorage.setItem('metrics.id', id); } let osName = os.platform(); let osVersion = util.isWindows() ? os.release() : osxRelease(os.release()).version; mixpanel.track(name, assign({ distinct_id: id, version: util.packagejson().version, 'Operating System': osName, 'Operating System Version': osVersion, 'Operating System Architecture': os.arch() }, data)); }, }; module.exports = Metrics; ================================================ FILE: src/utils/RegHubUtil.js ================================================ import {isNullOrUndefined} from 'util'; import _ from 'underscore'; import request from 'request'; import async from 'async'; import util from '../utils/Util'; import hubUtil from '../utils/HubUtil'; import repositoryServerActions from '../actions/RepositoryServerActions'; import tagServerActions from '../actions/TagServerActions'; import os from 'os'; var cachedRequest = require('cached-request')(request); var cacheDirectory = os.tmpdir() + '/cachekitematic'; cachedRequest.setCacheDirectory(cacheDirectory); cachedRequest.setValue('ttl', 3000); let REGHUB2_ENDPOINT = process.env.REGHUB2_ENDPOINT || 'https://hub.docker.com/v2'; let searchReq = null; let PAGING = 24; module.exports = { // Normalizes results from search to v2 repository results normalize: function (repo) { let obj = _.clone(repo); if (obj.is_official) { obj.namespace = 'library'; } else { let [namespace, name] = repo.name.split('/'); obj.namespace = namespace; obj.name = name; } return obj; }, search: function (query, page, sorting = null) { if (searchReq) { if (searchReq.request) { searchReq.request.abort(); } searchReq = null; } if (!query) { repositoryServerActions.resultsUpdated({repos: []}); } /** * Sort: * All - no sorting * ordering: -start_count * ordering: -pull_count * is_automated: 1 * is_official: 1 */ searchReq = cachedRequest({ url: `${REGHUB2_ENDPOINT}/search/repositories/?`, qs: {query: query, page: page, page_size: PAGING, sorting} }, (error, response, body) => { if (error) { repositoryServerActions.error({error}); } let data = JSON.parse(body); let repos = _.map(data.results, result => { result.name = result.repo_name; return this.normalize(result); }); let next = data.next; let previous = data.previous; let total = Math.floor(data.count / PAGING); if (response.statusCode === 200) { repositoryServerActions.resultsUpdated({repos, page, previous, next, total}); } }); }, recommended: function () { cachedRequest({ url: 'https://kitematic.com/recommended.json' }, (error, response, body) => { if (error) { repositoryServerActions.error({error}); return; } if (response.statusCode !== 200) { repositoryServerActions.error({error: new Error('Could not fetch recommended repo list. Please try again later.')}); return; } let data = JSON.parse(body); let repos = data.repos; async.map(repos, (repo, cb) => { var name = repo.repo; if (util.isOfficialRepo(name)) { name = 'library/' + name; } cachedRequest({ url: `${REGHUB2_ENDPOINT}/repositories/${name}`, timeout: 5000 }, (error, response, body) => { if (error) { return cb(null, {error, data: null}); } if (response.statusCode === 200) { let data = JSON.parse(body); data.is_recommended = true; _.extend(data, repo); return cb(null, {error: null, data}); } else { return cb(null, {error: new Error('Could not fetch repository information from Docker Hub.'), data: null}); } }); }, (error, repos) => { const reposData = repos.map(repo => repo.data).filter(repo => !isNullOrUndefined(repo)); if (!reposData.length) { const errorMessage =_.chain(repos) .map(repo => repo.error) .filter(err => !isNullOrUndefined(err)) .map(err => err.message) .uniq() .value() .join('\n'); repositoryServerActions.error({error: new Error(errorMessage)}); return; } repositoryServerActions.recommendedUpdated({repos: reposData}); }); }); }, tags: function (repo, callback) { hubUtil.request({ url: `${REGHUB2_ENDPOINT}/repositories/${repo}/tags`, qs: {page: 1, page_size: 100} }, (error, response, body) => { if (response.statusCode === 200) { let data = JSON.parse(body); tagServerActions.tagsUpdated({repo, tags: data.results || []}); if (callback) { return callback(null, data.results || []); } } else { repositoryServerActions.error({repo}); if (callback) { return callback(new Error('Failed to fetch tags for repo')); } } }); }, // Returns the base64 encoded index token or null if no token exists repos: function (callback) { repositoryServerActions.reposLoading({repos: []}); let namespaces = []; // Get Orgs for user hubUtil.request({ url: `${REGHUB2_ENDPOINT}/user/orgs/`, qs: { page_size: 1000 } }, (orgError, orgResponse, orgBody) => { if (orgError) { repositoryServerActions.error({orgError}); if (callback) { return callback(orgError); } return null; } if (orgResponse.statusCode === 401) { hubUtil.logout(); repositoryServerActions.reposUpdated({repos: []}); return; } if (orgResponse.statusCode !== 200) { let generalError = new Error('Failed to fetch repos'); repositoryServerActions.error({error: generalError}); if (callback) { callback({error: generalError}); } return null; } try { let orgs = JSON.parse(orgBody); orgs.results.map((org) => { namespaces.push(org.orgname); }); // Add current user namespaces.push(hubUtil.username()); } catch (jsonError) { repositoryServerActions.error({jsonError}); if (callback) { return callback(jsonError); } } async.map(namespaces, (namespace, cb) => { hubUtil.request({ url: `${REGHUB2_ENDPOINT}/repositories/${namespace}`, qs: { page_size: 1000 } }, (error, response, body) => { if (error) { repositoryServerActions.error({error}); if (callback) { callback(error); } return null; } if (orgResponse.statusCode === 401) { hubUtil.logout(); repositoryServerActions.reposUpdated({repos: []}); return; } if (response.statusCode !== 200) { repositoryServerActions.error({error: new Error('Could not fetch repository information from Docker Hub.')}); return null; } let data = JSON.parse(body); cb(null, data.results); }); }, (error, lists) => { if (error) { repositoryServerActions.error({error}); if (callback) { callback(error); } return null; } let repos = []; for (let list of lists) { repos = repos.concat(list); } _.each(repos, repo => { repo.is_user_repo = true; }); repositoryServerActions.reposUpdated({repos}); if (callback) { return callback(null, repos); } return null; }); }); } }; ================================================ FILE: src/utils/SetupUtil.js ================================================ import _ from 'underscore'; import fs from 'fs'; import path from 'path'; import Promise from 'bluebird'; import bugsnag from 'bugsnag-js'; import util from './Util'; import virtualBox from './VirtualBoxUtil'; import setupServerActions from '../actions/SetupServerActions'; import metrics from './MetricsUtil'; import machine from './DockerMachineUtil'; import docker from './DockerUtil'; import router from '../router'; // Docker Machine exits with 3 to differentiate pre-create check failures (e.g. // virtualization isn't enabled) from normal errors during create (exit code // 1). const precreateCheckExitCode = 3; let _retryPromise = null; let _timers = []; export default { simulateProgress (estimateSeconds) { this.clearTimers(); var times = _.range(0, estimateSeconds * 1000, 200); _.each(times, time => { var timer = setTimeout(() => { setupServerActions.progress({progress: 100 * time / (estimateSeconds * 1000)}); }, time); _timers.push(timer); }); }, clearTimers () { _timers.forEach(t => clearTimeout(t)); _timers = []; }, async useVbox () { metrics.track('Retried Setup with VBox'); router.get().transitionTo('loading'); util.native = false; localStorage.setItem('settings.useVM', true); setupServerActions.error({ error: { message: null }}); _retryPromise.resolve(); }, retry (removeVM) { metrics.track('Retried Setup', { removeVM }); router.get().transitionTo('loading'); setupServerActions.error({ error: { message: null }}); if (removeVM) { machine.rm().finally(() => { _retryPromise.resolve(); }); } else { _retryPromise.resolve(); } }, pause () { _retryPromise = Promise.defer(); return _retryPromise.promise; }, async setup () { while (true) { try { if (util.isNative()) { await this.nativeSetup(); } else { await this.nonNativeSetup(); } return; } catch (error) { metrics.track('Native Setup Failed'); setupServerActions.error({error}); bugsnag.notify('Native Setup Failed', error.message, { 'Docker Error': error.message }, 'info'); this.clearTimers(); await this.pause(); } } }, async nativeSetup () { while (true) { try { router.get().transitionTo('setup'); docker.setup('localhost'); setupServerActions.started({started: true}); this.simulateProgress(5); metrics.track('Native Setup Finished'); return docker.version(); } catch (error) { throw new Error(error); } } }, async nonNativeSetup () { let virtualBoxVersion = null; let machineVersion = null; while (true) { try { setupServerActions.started({started: false}); // Make sure virtualBox and docker-machine are installed let virtualBoxInstalled = virtualBox.installed(); let machineInstalled = machine.installed(); if (!virtualBoxInstalled || !machineInstalled) { router.get().transitionTo('setup'); if (!virtualBoxInstalled) { setupServerActions.error({error: 'VirtualBox is not installed. Please install it via the Docker Toolbox.'}); } else { setupServerActions.error({error: 'Docker Machine is not installed. Please install it via the Docker Toolbox.'}); } this.clearTimers(); await this.pause(); continue; } virtualBoxVersion = await virtualBox.version(); machineVersion = await machine.version(); setupServerActions.started({started: true}); metrics.track('Started Setup', { virtualBoxVersion, machineVersion }); let exists if (process.env.MACHINE_STORAGE_PATH) { exists = await virtualBox.vmExists(machine.name()) && fs.existsSync(path.join(process.env.MACHINE_STORAGE_PATH, 'machines', machine.name())); } else { exists = await virtualBox.vmExists(machine.name()) && fs.existsSync(path.join(util.home(), '.docker', 'machine', 'machines', machine.name())); } if (!exists) { router.get().transitionTo('setup'); setupServerActions.started({started: true}); this.simulateProgress(60); try { await machine.rm(); } catch (err) {} await machine.create(); } else { let state = await machine.status(); if (state !== 'Running') { router.get().transitionTo('setup'); setupServerActions.started({started: true}); if (state === 'Saved') { this.simulateProgress(10); } else if (state === 'Stopped') { this.simulateProgress(25); } else { this.simulateProgress(40); } await machine.start(); } } // Try to receive an ip address from machine, for at least to 80 seconds. let tries = 80, ip = null; while (!ip && tries > 0) { try { tries -= 1; console.log('Trying to fetch machine IP, tries left: ' + tries); ip = await machine.ip(); await Promise.delay(1000); } catch (err) {} } if (ip) { docker.setup(ip, machine.name()); await docker.version(); } else { throw new Error('Could not determine IP from docker-machine.'); } break; } catch (error) { router.get().transitionTo('setup'); if (error.code === precreateCheckExitCode) { metrics.track('Setup Halted', { virtualBoxVersion, machineVersion }); } else { metrics.track('Setup Failed', { virtualBoxVersion, machineVersion }); } let message = error.message.split('\n'); let lastLine = message.length > 1 ? message[message.length - 2] : 'Docker Machine encountered an error.'; let virtualBoxLogs = machine.virtualBoxLogs(); bugsnag.notify('Setup Failed', lastLine, { 'Docker Machine Logs': error.message, 'VirtualBox Logs': virtualBoxLogs, 'VirtualBox Version': virtualBoxVersion, 'Machine Version': machineVersion, groupingHash: machineVersion }, 'info'); setupServerActions.error({error: new Error(message)}); this.clearTimers(); await this.pause(); } } metrics.track('Setup Finished', { virtualBoxVersion, machineVersion }); } }; ================================================ FILE: src/utils/Util.js ================================================ import child_process from 'child_process'; import Promise from 'bluebird'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import http from 'http'; import electron from 'electron'; const remote = electron.remote; const dialog = remote.dialog; const app = remote.app; module.exports = { native: null, execFile: function (args, options) { return new Promise((resolve, reject) => { child_process.execFile(args[0], args.slice(1), options, (error, stdout) => { if (error) { reject(error); } else { resolve(stdout); } }); }); }, exec: function (args, options) { return new Promise((resolve, reject) => { child_process.exec(args, options, (error, stdout) => { if (error) { reject(new Error('Encountered an error: ' + error)); } else { resolve(stdout); } }); }); }, isWindows: function () { return process.platform === 'win32'; }, isLinux: function () { return process.platform === 'linux'; }, isNative: function () { switch (localStorage.getItem('settings.useVM')) { case 'true': this.native = false; break; case 'false': this.native = true; break; default: this.native = null; } if (this.native === null) { if (this.isWindows()) { this.native = http.get({ url: `http:////./pipe/docker_engine/version` }, (response) => { if (response.statusCode !== 200 ) { return false; } else { return true; } }); } else { try { // Check if file exists let stats = fs.statSync('/var/run/docker.sock'); if (stats.isSocket()) { this.native = true; } } catch (e) { if (this.isLinux()) { this.native = true; } else { this.native = false; } } } } return this.native; }, binsPath: function () { return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin'); }, binsEnding: function () { return this.isWindows() ? '.exe' : ''; }, dockerBinPath: function () { return path.join(this.binsPath(), 'docker' + this.binsEnding()); }, dockerMachineBinPath: function () { return path.join(this.binsPath(), 'docker-machine' + this.binsEnding()); }, dockerComposeBinPath: function () { return path.join(this.binsPath(), 'docker-compose' + this.binsEnding()); }, escapePath: function (str) { return str.replace(/ /g, '\\ ').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); }, home: function () { return app.getPath('home'); }, documents: function () { // TODO: fix me for windows 7 return 'Documents'; }, CommandOrCtrl: function () { return (this.isWindows() || this.isLinux()) ? 'Ctrl' : 'Command'; }, removeSensitiveData: function (str) { if (!str || str.length === 0 || typeof str !== 'string' ) { return str; } return str.replace(/-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/mg, '') .replace(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----/mg, '') .replace(/\/Users\/[^\/]*\//mg, '/Users//') .replace(/\\Users\\[^\/]*\\/mg, '\\Users\\\\'); }, packagejson: function () { return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); }, settingsjson: function () { var settingsjson = {}; try { settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) { // log errors } return settingsjson; }, isOfficialRepo: function (name) { if (!name || !name.length) { return false; } // An official repo is alphanumeric characters separated by dashes or // underscores. // Examples: myrepo, my-docker-repo, my_docker_repo // Non-examples: mynamespace/myrepo, my%!repo var repoRegexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; return repoRegexp.test(name); }, compareVersions: function (v1, v2, options) { var lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), v2parts = v2.split('.'); function isValidPart (x) { return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); } if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { return NaN; } if (zeroExtend) { while (v1parts.length < v2parts.length) { v1parts.push('0'); } while (v2parts.length < v1parts.length) { v2parts.push('0'); } } if (!lexicographical) { v1parts = v1parts.map(Number); v2parts = v2parts.map(Number); } for (var i = 0; i < v1parts.length; ++i) { if (v2parts.length === i) { return 1; } if (v1parts[i] === v2parts[i]) { continue; } else if (v1parts[i] > v2parts[i]) { return 1; } else { return -1; } } if (v1parts.length !== v2parts.length) { return -1; } return 0; }, randomId: function () { return crypto.randomBytes(32).toString('hex'); }, windowsToLinuxPath: function (windowsAbsPath) { var fullPath = windowsAbsPath.replace(':', '').split(path.sep).join('/'); if (fullPath.charAt(0) !== '/') { fullPath = '/' + fullPath.charAt(0).toLowerCase() + fullPath.substring(1); } return fullPath; }, linuxToWindowsPath: function (linuxAbsPath) { return linuxAbsPath.replace('/c', 'C:').split('/').join('\\'); }, linuxTerminal: function () { const terminalPath = localStorage.getItem('settings.terminalPath'); if (fs.existsSync(terminalPath)) { return [terminalPath, '-e']; } else { dialog.showMessageBox({ type: 'warning', buttons: ['OK'], message: `The ${terminalPath} does not exist please set the correct path.` }); return false; } }, webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983'] }; ================================================ FILE: src/utils/VirtualBoxUtil.js ================================================ import fs from 'fs'; import path from 'path'; import util from './Util'; import Promise from 'bluebird'; var VirtualBox = { command: function () { if (util.isWindows()) { if (process.env.VBOX_MSI_INSTALL_PATH) { return path.join(process.env.VBOX_MSI_INSTALL_PATH, 'VBoxManage.exe'); } else { return path.join(process.env.VBOX_INSTALL_PATH, 'VBoxManage.exe'); } } else { return '/Applications/VirtualBox.app/Contents/MacOS/VBoxManage'; } }, installed: function () { if (util.isWindows() && !process.env.VBOX_INSTALL_PATH && !process.env.VBOX_MSI_INSTALL_PATH) { return false; } return fs.existsSync(this.command()); }, active: function () { return fs.existsSync('/dev/vboxnetctl'); }, version: function () { return util.execFile([this.command(), '-v']).then(stdout => { let matchlist = stdout.match(/(\d+\.\d+\.\d+).*/); if (!matchlist || matchlist.length < 2) { Promise.reject('VBoxManage -v output format not recognized.'); } return Promise.resolve(matchlist[1]); }).catch(() => { return Promise.resolve(null); }); }, mountSharedDir: function (vmName, pathName, hostPath) { return util.execFile([this.command(), 'sharedfolder', 'add', vmName, '--name', pathName, '--hostpath', hostPath, '--automount']); }, vmExists: function (name) { return util.execFile([this.command(), 'list', 'vms']).then(out => { return out.indexOf('"' + name + '"') !== -1; }).catch(() => { return false; }); } }; module.exports = VirtualBox; ================================================ FILE: src/utils/WebUtil.js ================================================ import electron from 'electron'; const remote = electron.remote; const app = remote.app; import fs from 'fs'; import util from './Util'; import path from 'path'; import bugsnag from 'bugsnag-js'; import metrics from './MetricsUtil'; var WebUtil = { addWindowSizeSaving: function () { window.addEventListener('resize', function () { fs.writeFileSync(path.join(app.getPath('userData'), 'size'), JSON.stringify({ width: window.outerWidth, height: window.outerHeight })); }); }, addLiveReload: function () { if (process.env.NODE_ENV === 'development') { var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'http://localhost:35729/livereload.js'; head.appendChild(script); } }, addBugReporting: function () { var settingsjson = util.settingsjson(); if (settingsjson.bugsnag) { bugsnag.apiKey = settingsjson.bugsnag; bugsnag.autoNotify = true; bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; bugsnag.notifyReleaseStages = ['production']; bugsnag.appVersion = app.getVersion(); bugsnag.beforeNotify = function(payload) { if (!metrics.enabled()) { return false; } payload.stacktrace = util.removeSensitiveData(payload.stacktrace); payload.context = util.removeSensitiveData(payload.context); payload.file = util.removeSensitiveData(payload.file); payload.message = util.removeSensitiveData(payload.message); payload.url = util.removeSensitiveData(payload.url); payload.name = util.removeSensitiveData(payload.name); payload.file = util.removeSensitiveData(payload.file); for(var key in payload.metaData) { payload.metaData[key] = util.removeSensitiveData(payload.metaData[key]); } }; } }, disableGlobalBackspace: function () { document.onkeydown = function (e) { e = e || window.event; var doPrevent; if (e.keyCode === 8) { var d = e.srcElement || e.target; if (d.tagName.toUpperCase() === 'INPUT' || d.tagName.toUpperCase() === 'TEXTAREA') { doPrevent = d.readOnly || d.disabled; } else { doPrevent = true; } } else { doPrevent = false; } if (doPrevent) { e.preventDefault(); } }; }, }; module.exports = WebUtil; ================================================ FILE: styles/animation.less ================================================ @-webkit-keyframes spin { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } } @-webkit-keyframes translatewave { from { -webkit-transform: translateX(0px); } to { -webkit-transform: translateX(20px); } } @-webkit-keyframes translatedownload { 0% { -webkit-transform: translateY(6px); opacity: 0; } 25% { opacity: 1; -webkit-transform: translateY(6px); } 50% { opacity: 1; -webkit-transform: translateY(20px); } 100% { opacity: 1; -webkit-transform: translateY(20px); } } @-webkit-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } ================================================ FILE: styles/bootstrap/alerts.less ================================================ // // Alerts // -------------------------------------------------- // Base styles // ------------------------- .alert { padding: @alert-padding; margin-bottom: @line-height-computed; border: 1px solid transparent; border-radius: @alert-border-radius; // Headings for larger alerts h4 { margin-top: 0; // Specified for the h4 to prevent conflicts of changing @headings-color color: inherit; } // Provide class for links that match alerts .alert-link { font-weight: @alert-link-font-weight; } // Improve alignment and spacing of inner content > p, > ul { margin-bottom: 0; } > p + p { margin-top: 5px; } } // Dismissible alerts // // Expand the right padding and account for the close button's positioning. .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. .alert-dismissible { padding-right: (@alert-padding + 20); // Adjust close link position .close { position: relative; top: -2px; right: -21px; color: inherit; } } // Alternate styles // // Generate contextual modifier classes for colorizing the alert. .alert-success { .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); } .alert-info { .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); } .alert-warning { .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); } .alert-danger { .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); } ================================================ FILE: styles/bootstrap/badges.less ================================================ // // Badges // -------------------------------------------------- // Base class .badge { display: inline-block; min-width: 10px; padding: 3px 7px; font-size: @font-size-small; font-weight: @badge-font-weight; color: @badge-color; line-height: @badge-line-height; vertical-align: baseline; white-space: nowrap; text-align: center; background-color: @badge-bg; border-radius: @badge-border-radius; // Empty badges collapse automatically (not available in IE8) &:empty { display: none; } // Quick fix for badges in buttons .btn & { position: relative; top: -1px; } .btn-xs & { top: 0; padding: 1px 5px; } // Hover state, but only for links a& { &:hover, &:focus { color: @badge-link-hover-color; text-decoration: none; cursor: pointer; } } // Account for badges in navs .list-group-item.active > &, .nav-pills > .active > a > & { color: @badge-active-color; background-color: @badge-active-bg; } .list-group-item > & { float: right; } .list-group-item > & + & { margin-right: 5px; } .nav-pills > li > a > & { margin-left: 3px; } } ================================================ FILE: styles/bootstrap/bootstrap.less ================================================ // Core variables and mixins @import "variables.less"; @import "mixins.less"; // Reset and dependencies @import "normalize.less"; @import "print.less"; @import "glyphicons.less"; // Core CSS @import "scaffolding.less"; @import "type.less"; @import "code.less"; @import "grid.less"; @import "tables.less"; @import "forms.less"; @import "buttons.less"; // Components @import "component-animations.less"; @import "dropdowns.less"; @import "button-groups.less"; @import "input-groups.less"; @import "navs.less"; @import "navbar.less"; @import "breadcrumbs.less"; @import "pagination.less"; @import "pager.less"; @import "labels.less"; @import "badges.less"; @import "jumbotron.less"; @import "thumbnails.less"; @import "alerts.less"; @import "progress-bars.less"; @import "media.less"; @import "list-group.less"; @import "panels.less"; @import "responsive-embed.less"; @import "wells.less"; @import "close.less"; // Components w/ JavaScript @import "modals.less"; @import "tooltip.less"; @import "popovers.less"; @import "carousel.less"; // Utility classes @import "utilities.less"; @import "responsive-utilities.less"; ================================================ FILE: styles/bootstrap/breadcrumbs.less ================================================ // // Breadcrumbs // -------------------------------------------------- .breadcrumb { padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; margin-bottom: @line-height-computed; list-style: none; background-color: @breadcrumb-bg; border-radius: @border-radius-base; > li { display: inline-block; + li:before { content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space padding: 0 5px; color: @breadcrumb-color; } } > .active { color: @breadcrumb-active-color; } } ================================================ FILE: styles/bootstrap/button-groups.less ================================================ // // Button groups // -------------------------------------------------- // Make the div behave like a button .btn-group, .btn-group-vertical { position: relative; display: inline-block; vertical-align: middle; // match .btn alignment given font-size hack above > .btn { position: relative; float: left; // Bring the "active" button to the front &:hover, &:focus, &:active, &.active { z-index: 2; } } } // Prevent double borders when buttons are next to each other .btn-group { .btn + .btn, .btn + .btn-group, .btn-group + .btn, .btn-group + .btn-group { margin-left: -1px; } } // Optional: Group multiple button groups together for a toolbar .btn-toolbar { margin-left: -5px; // Offset the first child's margin &:extend(.clearfix all); .btn-group, .input-group { float: left; } > .btn, > .btn-group, > .input-group { margin-left: 5px; } } .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { border-radius: 0; } // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match .btn-group > .btn:first-child { margin-left: 0; &:not(:last-child):not(.dropdown-toggle) { .border-right-radius(0); } } // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { .border-left-radius(0); } // Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) .btn-group > .btn-group { float: left; } .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group > .btn-group:first-child { > .btn:last-child, > .dropdown-toggle { .border-right-radius(0); } } .btn-group > .btn-group:last-child > .btn:first-child { .border-left-radius(0); } // On active and open, don't show outline .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } // Sizing // // Remix the default button sizing classes into new ones for easier manipulation. .btn-group-xs > .btn { &:extend(.btn-xs); } .btn-group-sm > .btn { &:extend(.btn-sm); } .btn-group-lg > .btn { &:extend(.btn-lg); } // Split button dropdowns // ---------------------- // Give the line between buttons some depth .btn-group > .btn + .dropdown-toggle { padding-left: 8px; padding-right: 8px; } .btn-group > .btn-lg + .dropdown-toggle { padding-left: 12px; padding-right: 12px; } // The clickable button for toggling the menu // Remove the gradient and set the same inset shadow as the :active state .btn-group.open .dropdown-toggle { .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); // Show no shadow for `.btn-link` since it has no other button styles. &.btn-link { .box-shadow(none); } } // Reposition the caret .btn .caret { margin-left: 0; } // Carets in other button sizes .btn-lg .caret { border-width: @caret-width-large @caret-width-large 0; border-bottom-width: 0; } // Upside down carets for .dropup .dropup .btn-lg .caret { border-width: 0 @caret-width-large @caret-width-large; } // Vertical button groups // ---------------------- .btn-group-vertical { > .btn, > .btn-group, > .btn-group > .btn { display: block; float: none; width: 100%; max-width: 100%; } // Clear floats so dropdown menus can be properly placed > .btn-group { &:extend(.clearfix all); > .btn { float: none; } } > .btn + .btn, > .btn + .btn-group, > .btn-group + .btn, > .btn-group + .btn-group { margin-top: -1px; margin-left: 0; } } .btn-group-vertical > .btn { &:not(:first-child):not(:last-child) { border-radius: 0; } &:first-child:not(:last-child) { border-top-right-radius: @border-radius-base; .border-bottom-radius(0); } &:last-child:not(:first-child) { border-bottom-left-radius: @border-radius-base; .border-top-radius(0); } } .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group-vertical > .btn-group:first-child:not(:last-child) { > .btn:last-child, > .dropdown-toggle { .border-bottom-radius(0); } } .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { .border-top-radius(0); } // Justified button groups // ---------------------- .btn-group-justified { display: table; width: 100%; table-layout: fixed; border-collapse: separate; > .btn, > .btn-group { float: none; display: table-cell; width: 1%; } > .btn-group .btn { width: 100%; } > .btn-group .dropdown-menu { left: auto; } } // Checkbox and radio options // // In order to support the browser's form validation feedback, powered by the // `required` attribute, we have to "hide" the inputs via `clip`. We cannot use // `display: none;` or `visibility: hidden;` as that also hides the popover. // Simply visually hiding the inputs via `opacity` would leave them clickable in // certain cases which is prevented by using `clip` and `pointer-events`. // This way, we ensure a DOM element is visible to position the popover from. // // See https://github.com/twbs/bootstrap/pull/12794 and // https://github.com/twbs/bootstrap/pull/14559 for more information. [data-toggle="buttons"] { > .btn, > .btn-group > .btn { input[type="radio"], input[type="checkbox"] { position: absolute; clip: rect(0,0,0,0); pointer-events: none; } } } ================================================ FILE: styles/bootstrap/buttons.less ================================================ // // Buttons // -------------------------------------------------- // Base styles // -------------------------------------------------- .btn { display: inline-block; margin-bottom: 0; // For input.btn font-weight: @btn-font-weight; text-align: center; vertical-align: middle; touch-action: manipulation; cursor: pointer; background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 border: 1px solid transparent; white-space: nowrap; .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); .user-select(none); &, &:active, &.active { &:focus, &.focus { .tab-focus(); } } &:hover, &:focus, &.focus { color: @btn-default-color; text-decoration: none; } &:active, &.active { outline: 0; background-image: none; .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); } &.disabled, &[disabled], fieldset[disabled] & { cursor: @cursor-disabled; pointer-events: none; // Future-proof disabling of clicks .opacity(.65); .box-shadow(none); } } // Alternate buttons // -------------------------------------------------- .btn-default { .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); } .btn-primary { .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); } // Success appears as green .btn-success { .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); } // Info appears as blue-green .btn-info { .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); } // Warning appears as orange .btn-warning { .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); } // Danger and error appear as red .btn-danger { .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); } // Link buttons // ------------------------- // Make a button look and behave like a link .btn-link { color: @link-color; font-weight: normal; border-radius: 0; &, &:active, &.active, &[disabled], fieldset[disabled] & { background-color: transparent; .box-shadow(none); } &, &:hover, &:focus, &:active { border-color: transparent; } &:hover, &:focus { color: @link-hover-color; text-decoration: underline; background-color: transparent; } &[disabled], fieldset[disabled] & { &:hover, &:focus { color: @btn-link-disabled-color; text-decoration: none; } } } // Button Sizes // -------------------------------------------------- .btn-lg { // line-height: ensure even-numbered height of button next to large input .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); } .btn-sm { // line-height: ensure proper height of button next to small input .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); } .btn-xs { .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); } // Block button // -------------------------------------------------- .btn-block { display: block; width: 100%; } // Vertically space out multiple block buttons .btn-block + .btn-block { margin-top: 5px; } // Specificity overrides input[type="submit"], input[type="reset"], input[type="button"] { &.btn-block { width: 100%; } } ================================================ FILE: styles/bootstrap/carousel.less ================================================ // // Carousel // -------------------------------------------------- // Wrapper for the slide container and indicators .carousel { position: relative; } .carousel-inner { position: relative; overflow: hidden; width: 100%; > .item { display: none; position: relative; .transition(.6s ease-in-out left); // Account for jankitude on images > img, > a > img { &:extend(.img-responsive); line-height: 1; } // WebKit CSS3 transforms for supported devices @media all and (transform-3d), (-webkit-transform-3d) { transition: transform .6s ease-in-out; backface-visibility: hidden; perspective: 1000; &.next, &.active.right { transform: translate3d(100%, 0, 0); left: 0; } &.prev, &.active.left { transform: translate3d(-100%, 0, 0); left: 0; } &.next.left, &.prev.right, &.active { transform: translate3d(0, 0, 0); left: 0; } } } > .active, > .next, > .prev { display: block; } > .active { left: 0; } > .next, > .prev { position: absolute; top: 0; width: 100%; } > .next { left: 100%; } > .prev { left: -100%; } > .next.left, > .prev.right { left: 0; } > .active.left { left: -100%; } > .active.right { left: 100%; } } // Left/right controls for nav // --------------------------- .carousel-control { position: absolute; top: 0; left: 0; bottom: 0; width: @carousel-control-width; .opacity(@carousel-control-opacity); font-size: @carousel-control-font-size; color: @carousel-control-color; text-align: center; text-shadow: @carousel-text-shadow; // We can't have this transition here because WebKit cancels the carousel // animation if you trip this while in the middle of another animation. // Set gradients for backgrounds &.left { #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); } &.right { left: auto; right: 0; #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); } // Hover/focus state &:hover, &:focus { outline: 0; color: @carousel-control-color; text-decoration: none; .opacity(.9); } // Toggles .icon-prev, .icon-next, .glyphicon-chevron-left, .glyphicon-chevron-right { position: absolute; top: 50%; z-index: 5; display: inline-block; } .icon-prev, .glyphicon-chevron-left { left: 50%; margin-left: -10px; } .icon-next, .glyphicon-chevron-right { right: 50%; margin-right: -10px; } .icon-prev, .icon-next { width: 20px; height: 20px; margin-top: -10px; font-family: serif; } .icon-prev { &:before { content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) } } .icon-next { &:before { content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) } } } // Optional indicator pips // // Add an unordered list with the following class and add a list item for each // slide your carousel holds. .carousel-indicators { position: absolute; bottom: 10px; left: 50%; z-index: 15; width: 60%; margin-left: -30%; padding-left: 0; list-style: none; text-align: center; li { display: inline-block; width: 10px; height: 10px; margin: 1px; text-indent: -999px; border: 1px solid @carousel-indicator-border-color; border-radius: 10px; cursor: pointer; // IE8-9 hack for event handling // // Internet Explorer 8-9 does not support clicks on elements without a set // `background-color`. We cannot use `filter` since that's not viewed as a // background color by the browser. Thus, a hack is needed. // // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we // set alpha transparency for the best results possible. background-color: #000 \9; // IE8 background-color: rgba(0,0,0,0); // IE9 } .active { margin: 0; width: 12px; height: 12px; background-color: @carousel-indicator-active-bg; } } // Optional captions // ----------------------------- // Hidden by default for smaller viewports .carousel-caption { position: absolute; left: 15%; right: 15%; bottom: 20px; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: @carousel-caption-color; text-align: center; text-shadow: @carousel-text-shadow; & .btn { text-shadow: none; // No shadow for button elements in carousel-caption } } // Scale up controls for tablets and up @media screen and (min-width: @screen-sm-min) { // Scale up the controls a smidge .carousel-control { .glyphicon-chevron-left, .glyphicon-chevron-right, .icon-prev, .icon-next { width: 30px; height: 30px; margin-top: -15px; font-size: 30px; } .glyphicon-chevron-left, .icon-prev { margin-left: -15px; } .glyphicon-chevron-right, .icon-next { margin-right: -15px; } } // Show and left align the captions .carousel-caption { left: 20%; right: 20%; padding-bottom: 30px; } // Move up the indicators .carousel-indicators { bottom: 20px; } } ================================================ FILE: styles/bootstrap/close.less ================================================ // // Close icons // -------------------------------------------------- .close { float: right; font-size: (@font-size-base * 1.5); font-weight: @close-font-weight; line-height: 1; color: @close-color; text-shadow: @close-text-shadow; .opacity(.2); &:hover, &:focus { color: @close-color; text-decoration: none; cursor: pointer; .opacity(.5); } // Additional properties for button version // iOS requires the button element instead of an anchor tag. // If you want the anchor version, it requires `href="#"`. button& { padding: 0; cursor: pointer; background: transparent; border: 0; -webkit-appearance: none; } } ================================================ FILE: styles/bootstrap/code.less ================================================ // // Code (inline and block) // -------------------------------------------------- // Inline and block code styles code, kbd, pre, samp { font-family: @font-family-monospace; } // Inline code code { padding: 2px 4px; font-size: 90%; color: @code-color; background-color: @code-bg; border-radius: @border-radius-base; } // User input typically entered via keyboard kbd { padding: 2px 4px; font-size: 90%; color: @kbd-color; background-color: @kbd-bg; border-radius: @border-radius-small; box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); kbd { padding: 0; font-size: 100%; font-weight: bold; box-shadow: none; } } // Blocks of code pre { display: block; padding: ((@line-height-computed - 1) / 2); margin: 0 0 (@line-height-computed / 2); font-size: (@font-size-base - 1); // 14px to 13px line-height: @line-height-base; word-break: break-all; word-wrap: break-word; color: @pre-color; background-color: @pre-bg; border: 1px solid @pre-border-color; border-radius: @border-radius-base; // Account for some code outputs that place code tags in pre tags code { padding: 0; font-size: inherit; color: inherit; white-space: pre-wrap; background-color: transparent; border-radius: 0; } } // Enable scrollable blocks of code .pre-scrollable { max-height: @pre-scrollable-max-height; overflow-y: scroll; } ================================================ FILE: styles/bootstrap/component-animations.less ================================================ // // Component animations // -------------------------------------------------- // Heads up! // // We don't use the `.opacity()` mixin here since it causes a bug with text // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. .fade { opacity: 0; .transition(opacity .15s linear); &.in { opacity: 1; } } .collapse { display: none; visibility: hidden; &.in { display: block; visibility: visible; } tr&.in { display: table-row; } tbody&.in { display: table-row-group; } } .collapsing { position: relative; height: 0; overflow: hidden; .transition-property(~"height, visibility"); .transition-duration(.35s); .transition-timing-function(ease); } ================================================ FILE: styles/bootstrap/dropdowns.less ================================================ // // Dropdown menus // -------------------------------------------------- // Dropdown arrow/caret .caret { display: inline-block; width: 0; height: 0; margin-left: 2px; vertical-align: middle; border-top: @caret-width-base solid; border-right: @caret-width-base solid transparent; border-left: @caret-width-base solid transparent; } // The dropdown wrapper (div) .dropdown { position: relative; } // Prevent the focus on the dropdown toggle when closing dropdowns .dropdown-toggle:focus { outline: 0; } // The dropdown menu (ul) .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: @zindex-dropdown; display: none; // none by default, but block on "open" of the menu float: left; min-width: 160px; padding: 5px 0; margin: 2px 0 0; // override default ul list-style: none; font-size: @font-size-base; text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) background-color: @dropdown-bg; border: 1px solid @dropdown-fallback-border; // IE8 fallback border: 1px solid @dropdown-border; border-radius: @border-radius-base; .box-shadow(0 6px 12px rgba(0,0,0,.175)); background-clip: padding-box; // Aligns the dropdown menu to right // // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` &.pull-right { right: 0; left: auto; } // Dividers (basically an hr) within the dropdown .divider { .nav-divider(@dropdown-divider-bg); } // Links within the dropdown menu > li > a { display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: @line-height-base; color: @dropdown-link-color; white-space: nowrap; // prevent links from randomly breaking onto new lines } } // Hover/Focus state .dropdown-menu > li > a { &:hover, &:focus { text-decoration: none; color: @dropdown-link-hover-color; background-color: @dropdown-link-hover-bg; } } // Active state .dropdown-menu > .active > a { &, &:hover, &:focus { color: @dropdown-link-active-color; text-decoration: none; outline: 0; background-color: @dropdown-link-active-bg; } } // Disabled state // // Gray out text and ensure the hover/focus state remains gray .dropdown-menu > .disabled > a { &, &:hover, &:focus { color: @dropdown-link-disabled-color; } // Nuke hover/focus effects &:hover, &:focus { text-decoration: none; background-color: transparent; background-image: none; // Remove CSS gradient .reset-filter(); cursor: @cursor-disabled; } } // Open state for the dropdown .open { // Show the menu > .dropdown-menu { display: block; } // Remove the outline when :focus is triggered > a { outline: 0; } } // Menu positioning // // Add extra class to `.dropdown-menu` to flip the alignment of the dropdown // menu with the parent. .dropdown-menu-right { left: auto; // Reset the default from `.dropdown-menu` right: 0; } // With v3, we enabled auto-flipping if you have a dropdown within a right // aligned nav component. To enable the undoing of that, we provide an override // to restore the default dropdown menu alignment. // // This is only for left-aligning a dropdown menu within a `.navbar-right` or // `.pull-right` nav component. .dropdown-menu-left { left: 0; right: auto; } // Dropdown section headers .dropdown-header { display: block; padding: 3px 20px; font-size: @font-size-small; line-height: @line-height-base; color: @dropdown-header-color; white-space: nowrap; // as with > li > a } // Backdrop to catch body clicks on mobile, etc. .dropdown-backdrop { position: fixed; left: 0; right: 0; bottom: 0; top: 0; z-index: (@zindex-dropdown - 10); } // Right aligned dropdowns .pull-right > .dropdown-menu { right: 0; left: auto; } // Allow for dropdowns to go bottom up (aka, dropup-menu) // // Just add .dropup after the standard .dropdown class and you're set, bro. // TODO: abstract this so that the navbar fixed styles are not placed here? .dropup, .navbar-fixed-bottom .dropdown { // Reverse the caret .caret { border-top: 0; border-bottom: @caret-width-base solid; content: ""; } // Different positioning for bottom up menu .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 1px; } } // Component alignment // // Reiterate per navbar.less and the modified component alignment there. @media (min-width: @grid-float-breakpoint) { .navbar-right { .dropdown-menu { .dropdown-menu-right(); } // Necessary for overrides of the default right aligned menu. // Will remove come v4 in all likelihood. .dropdown-menu-left { .dropdown-menu-left(); } } } ================================================ FILE: styles/bootstrap/forms.less ================================================ // // Forms // -------------------------------------------------- // Normalize non-controls // // Restyle and baseline non-control form elements. fieldset { padding: 0; margin: 0; border: 0; // Chrome and Firefox set a `min-width: min-content;` on fieldsets, // so we reset that to ensure it behaves more like a standard block element. // See https://github.com/twbs/bootstrap/issues/12359. min-width: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: @line-height-computed; font-size: (@font-size-base * 1.5); line-height: inherit; color: @legend-color; border: 0; border-bottom: 1px solid @legend-border-color; } label { display: inline-block; max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) margin-bottom: 5px; font-weight: bold; } // Normalize form controls // // While most of our form styles require extra classes, some basic normalization // is required to ensure optimum display with or without those classes to better // address browser inconsistencies. // Override content-box in Normalize (* isn't specific enough) input[type="search"] { .box-sizing(border-box); } // Position radios and checkboxes better input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; margin-top: 1px \9; // IE8-9 line-height: normal; } // Set the height of file controls to match text inputs input[type="file"] { display: block; } // Make range inputs behave like textual form controls input[type="range"] { display: block; width: 100%; } // Make multiple select elements height not fixed select[multiple], select[size] { height: auto; } // Focus for file, radio, and checkbox input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { .tab-focus(); } // Adjust output element output { display: block; padding-top: (@padding-base-vertical + 1); font-size: @font-size-base; line-height: @line-height-base; color: @input-color; } // Common form controls // // Shared size and type resets for form controls. Apply `.form-control` to any // of the following form controls: // // select // textarea // input[type="text"] // input[type="password"] // input[type="datetime"] // input[type="datetime-local"] // input[type="date"] // input[type="month"] // input[type="time"] // input[type="week"] // input[type="number"] // input[type="email"] // input[type="url"] // input[type="search"] // input[type="tel"] // input[type="color"] .form-control { display: block; width: 100%; height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) padding: @padding-base-vertical @padding-base-horizontal; font-size: @font-size-base; line-height: @line-height-base; color: @input-color; background-color: @input-bg; background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 border: 1px solid @input-border; border-radius: @input-border-radius; .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); // Customize the `:focus` state to imitate native WebKit styles. .form-control-focus(); // Placeholder .placeholder(); // Disabled and read-only inputs // // HTML5 says that controls under a fieldset > legend:first-child won't be // disabled if the fieldset is disabled. Due to implementation difficulty, we // don't honor that edge case; we style them as disabled anyway. &[disabled], &[readonly], fieldset[disabled] & { cursor: @cursor-disabled; background-color: @input-bg-disabled; opacity: 1; // iOS fix for unreadable disabled content } // Reset height for `textarea`s textarea& { height: auto; } } // Search inputs in iOS // // This overrides the extra rounded corners on search inputs in iOS so that our // `.form-control` class can properly style them. Note that this cannot simply // be added to `.form-control` as it's not specific enough. For details, see // https://github.com/twbs/bootstrap/issues/11586. input[type="search"] { -webkit-appearance: none; } // Special styles for iOS temporal inputs // // In Mobile Safari, setting `display: block` on temporal inputs causes the // text within the input to become vertically misaligned. As a workaround, we // set a pixel line-height that matches the given height of the input, but only // for Safari. @media screen and (-webkit-min-device-pixel-ratio: 0) { input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] { line-height: @input-height-base; } input[type="date"].input-sm, input[type="time"].input-sm, input[type="datetime-local"].input-sm, input[type="month"].input-sm { line-height: @input-height-small; } input[type="date"].input-lg, input[type="time"].input-lg, input[type="datetime-local"].input-lg, input[type="month"].input-lg { line-height: @input-height-large; } } // Form groups // // Designed to help with the organization and spacing of vertical forms. For // horizontal forms, use the predefined grid classes. .form-group { margin-bottom: 15px; } // Checkboxes and radios // // Indent the labels to position radios/checkboxes as hanging controls. .radio, .checkbox { position: relative; display: block; margin-top: 10px; margin-bottom: 10px; label { min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text padding-left: 20px; margin-bottom: 0; font-weight: normal; cursor: pointer; } } .radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { position: absolute; margin-left: -20px; margin-top: 4px \9; } .radio + .radio, .checkbox + .checkbox { margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing } // Radios and checkboxes on same line .radio-inline, .checkbox-inline { display: inline-block; padding-left: 20px; margin-bottom: 0; vertical-align: middle; font-weight: normal; cursor: pointer; } .radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline { margin-top: 0; margin-left: 10px; // space out consecutive inline controls } // Apply same disabled cursor tweak as for inputs // Some special care is needed because