Showing preview only (606K chars total). Download the full file or copy to clipboard to get everything.
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/)

### 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 <frenchben@docker.com>',
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.
[](https://travis-ci.org/docker/kitematic)
[](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 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('<redacted>')).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('<redacted>')).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/<redacted>/')).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/<redacted>/.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('<redacted>')).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
================================================
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="main.css"/>
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';">
<title>Kitematic</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
================================================
FILE: jest-integration.json
================================================
{
"testMatch": ["**/__integration__/**/*.js"],
"transform": {".*": "<rootDir>/node_modules/babel-jest"},
"setupFiles": ["<rootDir>/util/testenv.js"],
"setupTestFrameworkScriptFile": "<rootDir>/util/prepare.js",
"unmockedModulePathPatterns": [
"babel",
"core-js",
"<rootDir>/node_modules/source-map-support"
]
}
================================================
FILE: jest-unit.json
================================================
{
"transform": { ".*": "<rootDir>/node_modules/babel-jest" },
"setupFiles": ["<rootDir>/util/testenv.js"],
"setupTestFrameworkScriptFile": "<rootDir>/util/prepare.js",
"unmockedModulePathPatterns": [
"alt",
"stream",
"tty",
"net",
"crypto",
"babel",
"bluebird",
"object-assign",
"underscore",
"source-map-support",
"<rootDir>/node_modules/.*JSONStream",
"<rootDir>/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 <earnie@users.sf.net>
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 <ylo@cs.hut.fi>, 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 <futo@core-sdi.com>
* <http://www.core-sdi.com>
3)
ssh-keyscan was contributed by David Mazieres under a BSD-style
license.
* Copyright 1995, 1996 by David Mazieres <dm@lcs.mit.edu>.
*
* 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 <vincent.rijmen@esat.kuleuven.ac.be>
* @author Antoon Bosselaers <antoon.bosselaers@esat.kuleuven.ac.be>
* @author Paulo Barreto <paulo.barreto@terra.com.br>
*
* 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 <<EOF
set doesExist to false
try
do shell script "osascript -e 'exists application \"iTerm\"'"
set doesExist to true
end try
return doesExist
EOF`
function open_iterm () {
osascript > /dev/null <<EOF
tell application "iTerm"
activate
try
tell the first terminal
launch session "Default Session"
tell the last session
write text "bash -c \"$CMD\""
end tell
end tell
on error
tell (make new terminal)
launch session "Default Session"
tell the last session
write text "bash -c \"$CMD\""
end tell
end tell
end try
end tell
EOF
}
function open_iterm2point9 () {
osascript > /dev/null <<EOF
tell application "iTerm"
if version ≥ 2.9 then
activate
tell current window
create tab with default profile
tell first session of current tab
write text "bash -c \"$CMD\""
end tell
end tell
end if
end tell
EOF
}
function open_terminal () {
osascript > /dev/null <<EOF
tell application "Terminal" to activate
delay 0.4
tell application "System Events" to keystroke "t" using command down
tell application "Terminal"
do script "bash -c \"$CMD\"" in window 1
end tell
EOF
}
if [ "$ITERM_EXISTS" == "true" ]; then
open_iterm2point9 "$@" || open_iterm "$@" || open_terminal "$@"
else
open_terminal "$@"
fi
================================================
FILE: src/actions/AccountActions.js
================================================
import alt from '../alt';
import hub from '../utils/HubUtil';
class AccountActions {
login (username, password) {
this.dispatch({});
hub.login(username, password);
}
signup (username, password, email, subscribe) {
this.dispatch({});
hub.signup(username, password, email, subscribe);
}
logout () {
this.dispatch({});
hub.logout();
}
skip () {
this.dispatch({});
hub.setPrompted(true);
}
verify () {
this.dispatch({});
hub.verify();
}
}
export default alt.createActions(AccountActions);
================================================
FILE: src/actions/AccountServerActions.js
================================================
import alt from '../alt';
class AccountServerActions {
constructor () {
this.generateActions(
'signedup',
'loggedin',
'loggedout',
'prompted',
'errors',
'verified'
);
}
}
export default alt.createActions(AccountServerActions);
================================================
FILE: src/actions/ContainerActions.js
================================================
import alt from '../alt';
import dockerUtil from '../utils/DockerUtil';
import _ from "underscore";
class ContainerActions {
destroy (name) {
dockerUtil.destroy(name);
}
rename (name, newName) {
this.dispatch({name, newName});
dockerUtil.rename(name, newName);
}
start (name) {
this.dispatch({name});
dockerUtil.start(name);
}
stop (name) {
dockerUtil.stop(name);
}
restart (name) {
this.dispatch({name});
dockerUtil.restart(name);
}
update (name, container) {
this.dispatch({name, container});
dockerUtil.updateContainer(name, container);
}
clearPending () {
this.dispatch();
}
run (name, repo, tag, network, local=false) {
dockerUtil.run(name, repo, tag, network, local);
}
active (name) {
dockerUtil.active(name);
}
toggleFavorite (name) {
let favorites = JSON.parse(localStorage.getItem('containers.favorites')) || [];
if (favorites.includes(name)) {
favorites = favorites.filter(favoriteName => 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(<Handler/>, 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 (
<div className="preferences">
<div className="about-content">
<a onClick={this.handleGoBackClick}>Go Back</a>
<div className="items">
<div className="item">
<RetinaImage src="cartoon-kitematic.png"/>
<h4>Docker {packages.name}</h4>
<p>{packages.version}</p>
</div>
</div>
<h3>Kitematic is built with:</h3>
<div className="items">
<div className="item">
<RetinaImage src="cartoon-docker.png"/>
<h4>Docker Engine</h4>
</div>
<div className="item">
<RetinaImage src="cartoon-docker-machine.png"/>
<h4>Docker Machine</h4>
<p>{packages["docker-machine-version"]}</p>
</div>
</div>
<h3>Third-Party Software</h3>
<div className="items">
<div className="item">
<h4>VirtualBox</h4>
<p>{packages["virtualbox-version"]}</p>
</div>
</div>
<div className="items">
<div className="item">
<h4>Electron</h4>
<p>{packages["electron-version"]}</p>
</div>
</div>
</div>
</div>
);
}
});
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 ?
<a className="btn btn-action btn-close" disabled={this.state.loading} onClick={this.handleClose}>Close</a> :
<a className="btn btn-action btn-skip" disabled={this.state.loading} onClick={this.handleSkip}>Skip For Now</a>;
return (
<div className="setup">
<Header hideLogin={true}/>
<div className="setup-content">
{close}
<div className="form-section">
<RetinaImage src={'connect-to-hub.png'} checkIfRetinaImgExists={false}/>
<Router.RouteHandler errors={this.state.errors} loading={this.state.loading} {...this.props}/>
</div>
<div className="desc">
<div className="content">
<h1>Connect to Docker Hub</h1>
<p>Pull and run private Docker Hub images by connecting your Docker Hub account to Kitematic.</p>
</div>
</div>
</div>
</div>
);
}
});
================================================
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 ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
return (
<form className="form-connect">
<input ref="usernameInput"maxLength="30" name="username" placeholder="Username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.username}</p>
<input ref="passwordInput" name="password" placeholder="Password" type="password" disabled={this.props.loading} valueLink={this.linkState('password')} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.password}</p>
<a className="link" onClick={this.handleClickForgotPassword}>Forgot your password?</a>
<p className="error-message">{this.state.errors.detail}</p>
<div className="submit">
{loading}
<button className="btn btn-action" disabled={this.props.loading} onClick={this.handleLogin} type="submit">Log In</button>
</div>
<br/>
<div className="extra">Don't have an account yet? <a disabled={this.state.loading} onClick={this.handleClickSignup}>Sign Up</a></div>
</form>
);
}
});
================================================
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 ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
return (
<form className="form-connect" onSubmit={this.handleSignUp}>
<input ref="usernameInput" maxLength="30" name="username" placeholder="Username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.username}</p>
<input ref="emailInput" name="email" placeholder="Email" type="text" valueLink={this.linkState('email')} disabled={this.props.loading} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.email}</p>
<input ref="passwordInput" name="password" placeholder="Password" type="password" valueLink={this.linkState('password')} disabled={this.props.loading} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.password}</p>
<div className="checkbox">
<label>
<input type="checkbox" disabled={this.props.loading} checkedLink={this.linkState('subscribe')}/> Subscribe to the Docker newsletter.
</label>
</div>
<p className="error-message">{this.state.errors.detail}</p>
<div className="submit">
{loading}
<button className="btn btn-action" disabled={this.props.loading} type="submit">Sign Up</button>
</div>
<br/>
<div className="extra">Already have an account? <a disabled={this.state.loading} onClick={this.handleClickLogin}>Log In</a></div>
</form>
);
}
});
================================================
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 (
<div className="details">
<ContainerDetailsHeader {...this.props} defaultPort={defaultPort} ports={ports}/>
<ContainerDetailsSubheader {...this.props} defaultPort={defaultPort} ports={ports}/>
<Router.RouteHandler {...this.props} defaultPort={defaultPort} ports={ports}/>
</div>
);
}
});
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 = <span className="status downloading">UPDATING</span>;
} else if (this.props.container.State.Stopping) {
state = <span className="status running">STOPPING</span>;
} else if (this.props.container.State.Paused) {
state = <span className="status paused">PAUSED</span>;
} else if (this.props.container.State.Restarting) {
state = <span className="status restarting">RESTARTING</span>;
} else if (this.props.container.State.Running && !this.props.container.State.ExitCode) {
state = <span className="status running">RUNNING</span>;
} else if (this.props.container.State.Starting) {
state = <span className="status running">STARTING</span>;
} else if (this.props.container.State.Downloading) {
state = <span className="status downloading">DOWNLOADING</span>;
} else {
state = <span className="status stopped">STOPPED</span>;
}
return (
<div className="header-section">
<div className="text">
{this.props.container.Name}{state}
</div>
</div>
);
}
});
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 = (
<div className={startActionClass}>
<div className="action-icon start" onClick={this.handleStart}><span className="icon icon-start"></span></div>
<div className="btn-label">START</div>
</div>
);
} else {
startStopToggle = (
<div className={stopActionClass}>
<div className="action-icon stop" onClick={this.handleStop}><span className="icon icon-stop"></span></div>
<div className="btn-label">STOP</div>
</div>
);
}
return (
<div className="details-subheader">
<div className="details-header-actions">
{startStopToggle}
<div className={restartActionClass}>
<div className="action-icon" onClick={this.handleRestart}><span className="icon icon-restart"></span></div>
<div className="btn-label">RESTART</div>
</div>
<div className={terminalActionClass}>
<div className="action-icon" onClick={this.handleTerminal}><span className="icon icon-docker-exec"></span></div>
<div className="btn-label">EXEC</div>
</div>
<div className={docsActionClass}>
<div className="action-icon" onClick={this.handleDocs}><span className="icon icon-open-external"></span></div>
<div className="btn-label">DOCS</div>
</div>
</div>
<div className="details-subheader-tabs">
<span className={tabHomeClasses} onClick={this.showHome}>Home</span>
<span className={tabSettingsClasses} onClick={this.showSettings}>Settings</span>
</div>
</div>
);
}
});
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 = (
<div className="details-progress error">
<h2>We're sorry. There seems to be an error:</h2>
{error.split('\n').map(i => {
return <p className="error-message">{i}</p>;
})}
<p>If this error is invalid, please file a ticket on our Github repo.</p>
<a className="btn btn-action" onClick={this.handleErrorClick}>File Ticket</a>
</div>
);
} 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 = (
<div className="details-progress">
<h2>{total >= 100 ? 'Creating Container' : 'Downloading Image'}</h2>
<h2>{total}%</h2>
<div className="container-progress-wrapper">
<ContainerProgress pBar1={values[0]} pBar2={values[1]} pBar3={values[2]} pBar4={values[3]}/>
</div>
</div>
);
} else if (this.props.container.State.Waiting) {
body = (
<div className="details-progress">
<h2>Waiting For Another Download</h2>
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
);
} else {
body = (
<div className="details-progress">
<h2>Connecting to Docker Hub</h2>
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
);
}
} else {
var logWidget = (
<ContainerHomeLogs container={this.props.container}/>
);
var folderWidget;
if (this.showFolders()) {
folderWidget = (
<ContainerHomeFolders container={this.props.container} />
);
}
if (logWidget && !folderWidget) {
body = (
<div className="details-panel home">
<div className="content">
<div className="full">
{logWidget}
</div>
</div>
</div>
);
} else {
body = (
<div className="details-panel home">
<div className="content">
<div className="left">
{logWidget}
</div>
<div className="right">
{folderWidget}
</div>
</div>
</div>
);
}
}
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 (
<div key={i} className="folder" onClick={this.handleClickFolder.bind(this, source, destination)}>
<RetinaImage src="folder.png" />
<div className="text">{destination}</div>
</div>
);
});
return (
<div className="folders wrapper">
<div className="widget">
<div className="top-bar">
<div className="text">Volumes</div>
<div className="action" onClick={this.handleClickChangeFolders}>
<span className="icon icon-preferences"></span>
</div>
</div>
<div className="folders-list">
{folders}
</div>
</div>
</div>
);
}
});
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 (
<tr key={key}>
<td>{key + '/' + val.portType}</td>
<td>{val.url}</td>
</tr>
);
});
return (
<div className="web-preview wrapper">
<div className="widget">
<div className="top-bar">
<div className="text">IP & PORTS</div>
<div className="action" onClick={this.handleClickPortSettings}>
<span className="icon icon-preferences"></span>
</div>
</div>
<p>You can access this container using the following IP address and port:</p>
<table className="table">
<thead>
<tr>
<th>DOCKER PORT</th>
<th>ACCESS URL</th>
</tr>
</thead>
<tbody>
{ports}
</tbody>
</table>
</div>
</div>
);
}
});
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(<option key={i} value={i}>{i+' px'}</option>);
}
return options;
},
render: function(){
return (
<select className='logs-font-size__select' value={this.props.fontSize} onChange={this.props.onChange}>
<option disabled="true" >Font size</option>
{this.getFontSizes(10, 30)}
</select>
);
}
});
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 <div key={key} dangerouslySetInnerHTML={{__html: convert.toHtml(escape(l.substr(l.indexOf(' ')+1)).replace(/ /g, ' <wbr>'))}}></div>;
}) : ['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 (
<div className="mini-logs wrapper">
<div className="widget">
<div className="top-bar">
<div className="text">Container Logs</div>
<div>
<label className="follow-logs__label">
Follow
<input type="checkbox" onChange={ this.toggleFollow } checked={ this.state.follow }></input>
</label>
<button className="save-logs__btn" onClick={saveLogs}>
<i className="icon icon-download"></i>
</button>
<FontSelect fontSize={this.state.fontSize} onChange={this.onFontChange} />
<button className="copy-logs__btn" onClick={copyLogs}>Copy</button>
</div>
</div>
<div className="logs" style={{fontSize:this.state.fontSize+'px'}}>
{logs}
</div>
</div>
</div>
);
}
});
================================================
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 (
<ContainerListItem key={container.Id} container={container} start={this.start} />
);
});
return (
<ul>
{containers}
</ul>
);
}
});
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 = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>{container.Config.Image}</Tooltip>}>
<span>{repo}</span>
</OverlayTrigger>
);
// Synchronize all animations
var style = {
WebkitAnimationDelay: 0 + 'ms'
};
var state;
if (container.State.Downloading) {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Downloading</Tooltip>}>
<div className="state state-downloading">
<div style={style} className="downloading-arrow"></div>
</div>
</OverlayTrigger>
);
} else if (container.State.Running && !container.State.Paused) {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Running</Tooltip>}>
<div className="state state-running"><div style={style} className="runningwave"></div></div>
</OverlayTrigger>
);
} else if (container.State.Restarting) {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Restarting</Tooltip>}>
<div className="state state-restarting" style={style}></div>
</OverlayTrigger>
);
} else if (container.State.Paused) {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Paused</Tooltip>}>
<div className="state state-paused"></div>
</OverlayTrigger>
);
} else if (container.State.ExitCode) {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Stopped</Tooltip>}>
<div className="state state-stopped"></div>
</OverlayTrigger>
);
} else {
state = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Stopped</Tooltip>}>
<div className="state state-stopped"></div>
</OverlayTrigger>
);
}
return (
<Router.Link to="container" params={{name: container.Name}}>
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave} onClick={self.handleClick} id={this.props.key}>
{state}
<div className="info">
<div className="name">
{container.Name}
</div>
<div className="image">
{imageName}
</div>
</div>
<div className="action">
<span className={container.Favorite ? 'btn circular favorite' : 'btn circular'} onClick={this.toggleFavoriteContainer}><span className="icon icon-favorite"></span></span>
<span className="btn circular" onClick={this.handleDeleteContainer}><span className="icon icon-delete"></span></span>
</div>
</li>
</Router.Link>
);
}
});
module.exports = ContainerListItem;
================================================
FILE: src/components/ContainerProgress.react.js
================================================
import React from 'react';
/*
Usage: <ContainerProgress pBar1={20} pBar2={70} pBar3={100} pBar4={20} />
*/
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 (
<div className="container-progress">
<div className="bar-1 bar-bg">
<div className="bar-fg" style={pBar4Style}></div>
</div>
<div className="bar-2 bar-bg">
<div className="bar-fg" style={pBar3Style}></div>
</div>
<div className="bar-3 bar-bg">
<div className="bar-fg" style={pBar2Style}></div>
</div>
<div className="bar-4 bar-bg">
<div className="bar-fg" style={pBar1Style}></div>
</div>
</div>
);
}
});
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 (<div></div>);
}
return (
<div className="details-panel">
<div className="settings">
<div className="settings-menu">
<ul>
<Router.Link to="containerSettingsGeneral" params={{name: container.Name}}>
<li>
General
</li>
</Router.Link>
<Router.Link to="containerSettingsPorts" params={{name: container.Name}}>
<li>
Hostname / Ports
</li>
</Router.Link>
<Router.Link to="containerSettingsVolumes" params={{name: container.Name}}>
<li>
Volumes
</li>
</Router.Link>
<Router.Link to="containerSettingsNetwork" params={{name: container.Name}}>
<li>
Network
</li>
</Router.Link>
<Router.Link to="containerSettingsAdvanced" params={{name: container.Name}}>
<li>
Advanced
</li>
</Router.Link>
</ul>
</div>
<Router.RouteHandler {...this.props}/>
</div>
</div>
);
}
});
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 (
<div className="settings-panel">
<div className="settings-section">
<h3>Advanced Options</h3>
<div className="checkboxes">
<p><label><input type="checkbox" checked={this.state.tty} onChange={this.handleChangeTty}/>Allocate a TTY for this container</label></p>
<p><label><input type="checkbox" checked={this.state.openStdin} onChange={this.handleChangeOpenStdin}/>Keep STDIN open even if not attached</label></p>
<p><label><input type="checkbox" checked={this.state.privileged} onChange={this.handleChangePrivileged}/>Privileged mode</label></p>
<p><label><input type="checkbox" checked={this.state.restartPolicy} onChange={this.handleChangeRestartPolicy}/>Enable 'always' restart policy</label></p>
</div>
<a className="btn btn-action" disabled={this.props.container.State.Updating} onClick={this.handleSaveAdvancedOptions}>Save</a>
</div>
</div>
);
}
});
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<l ; i++) {
name = name.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
name = name.replace(/[^a-z0-9-_.\s]/g, '') // Remove invalid chars
.replace(/\s+/g, '-') // Collapse whitespace and replace by -
.replace(/-+/g, '-') // Collapse dashes
.replace(/_+/g, '_'); // Collapse underscores
this.setState({
slugName: name
});
},
handleNameOnKeyUp: function (e) {
if (e.keyCode === 13 && this.state.slugName) {
this.handleSaveContainerName();
}
},
handleCopyContainerId: function() {
clipboard.writeText(this.props.container.Id);
this.setState({
copiedId: true
});
var _this = this;
setTimeout(function() {
_this.setState({
copiedId: false
});
}, 5000);
},
handleSaveContainerName: function () {
var newName = this.state.slugName;
if (newName === this.props.container.Name) {
return;
}
this.setState({
slugName: null
});
if (this.props.containers[newName]) {
this.setState({
nameError: 'A container already exists with this name.'
});
return;
}
containerActions.rename(this.props.container.Name, newName);
this.context.router.transitionTo('containerSettingsGeneral', {name: newName});
metrics.track('Changed Container Name');
},
handleSaveEnvVars: function () {
metrics.track('Saved Environment Variables');
let list = [];
_.each(this.state.env, kvp => {
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 = (
<a className="btn btn-action" onClick={this.handleSaveContainerName} disabled="disabled">Save</a>
);
if (this.state.slugName) {
willBeRenamedAs = (
<p>Will be renamed as: <strong>{this.state.slugName}</strong></p>
);
btnSaveName = (
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
);
} else if (this.state.nameError) {
willBeRenamedAs = (
<p><strong>{this.state.nameError}</strong></p>
);
}
if (this.state.copiedId) {
clipboardStatus = (
<p className="fadeOut"><strong>Copied to Clipboard</strong></p>
);
}
let containerInfo = (
<div className="settings-section">
<h3>Container Info</h3>
<div className="container-info-row">
<div className="label-id">ID</div>
<input type="text" className="line disabled" defaultValue={this.props.container.Id} disabled></input>
<a className="btn btn-action btn-copy" onClick={this.handleCopyContainerId}>Copy</a>
{clipboardStatus}
</div>
<div className="container-info-row">
<div className="label-name">NAME</div>
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name} onChange={this.handleNameChange} onKeyUp={this.handleNameOnKeyUp}></input>
{btnSaveName}
{willBeRenamedAs}
</div>
</div>
);
let vars = _.map(this.state.env, (kvp, index) => {
let [id, key, val] = kvp;
let icon;
if (index === this.state.env.length - 1) {
icon = <a onClick={this.handleAddEnvVar} className="only-icon btn btn-positive small"><span className="icon icon-add"></span></a>;
} else {
icon = <a onClick={this.handleRemoveEnvVar.bind(this, index)} className="only-icon btn btn-action small"><span className="icon icon-delete"></span></a>;
}
return (
<div key={id} className="keyval-row">
<input type="text" className="key line" defaultValue={key} onChange={this.handleChangeEnvKey.bind(this, index)}></input>
<input type="text" className="val line" defaultValue={val} onChange={this.handleChangeEnvVal.bind(this, index)}></input>
{icon}
</div>
);
});
return (
<div className="settings-panel">
{containerInfo}
<div className="settings-section">
<h3>Environment Variables</h3>
<div className="env-vars-labels">
<div className="label-key">KEY</div>
<div className="label-val">VALUE</div>
</div>
<div className="env-vars">
{vars}
</div>
<a className="btn btn-action" disabled={this.props.container.State.Updating} onClick={this.handleSaveEnvVars}>Save</a>
</div>
<div className="settings-section">
<h3>Delete Container</h3>
<a className="btn btn-action" onClick={this.handleDeleteContainer}>Delete Container</a>
</div>
</div>
);
}
});
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 (
<tr key={network.Id}>
<td><input type="checkbox" disabled={isUpdating || this.state.usedNetworks.host} name={network.Name} checked={this.state.usedNetworks[network.Name]} onChange={this.handleToggleNetwork}/></td>
<td>{network.Name}</td>
<td>{network.Driver}</td>
</tr>
)
}
});
let links = _.map(this.state.links, (link, key) => {
return (
<tr>
<td>{link.container}</td>
<td>{link.alias}</td>
<td>
<Router.Link to="container" params={{name: link.container}}>
<a className="btn btn-action small">OPEN</a>
</Router.Link>
<a name={key} className="btn btn-action small" onClick={this.handleRemoveLink}>REMOVE</a>
</td>
</tr>
)
})
let containerOptions = _.map(this.state.containers, (container) => {
return (
<option value={container.Name}>{container.Name}</option>
)
})
return (
<div className="settings-panel">
<div className="settings-section">
<h3>Configure network</h3>
<table className="table volumes">
<thead>
<tr>
<th> </th>
<th>NAME</th>
<th>DRIVER</th>
</tr>
</thead>
<tbody>
{networks}
</tbody>
</table>
{ !this.state.usedNetworks.host ? <a className="btn btn-action" disabled={isUpdating} onClick={this.handleSaveNetworkOptions}>Save</a> : null }
{ this.state.usedNetworks.host ? <span>You cannot configure networks while container connected to host network</span> : null }
</div>
<div className="settings-section">
<h3>Host network</h3>
{ !this.state.usedNetworks.host ? <a className="btn btn-action" disabled={isUpdating} onClick={this.handleToggleHostNetwork}>Connect to host network</a> : null }
{ this.state.usedNetworks.host ? <a className="btn btn-action" disabled={isUpdating} onClick={this.handleToggleHostNetwork}>Disconnect from host network</a> : null }
</div>
<div className="settings-section">
<h3>Links</h3>
<table className="table links">
<thead>
<tr>
<th>NAME</th>
<th>ALIAS</th>
<th> </th>
</tr>
</thead>
<tbody>
{links}
<tr>
<td>
<select className="line" value={this.state.newLink.container} onChange={this.handleNewLinkContainerChange}>
<option disabled value="">Select container</option>
{containerOptions}
</select>
</td>
<td>
<input id="new-link-alias" type="text" className="line" value={this.state.newLink.alias} onChange={this.handleNewLinkAliasChange} />
</td>
<td>
<a className="only-icon btn btn-positive small" disabled={!this.state.isNewLinkValid} onClick={this.handleNewLink}>
<span className="icon icon-add"></span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
});
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) ? (<a onClick={this.handleViewLink.bind(this, url)}>{ip}</a>) : ({ip});
var icon = '';
var portKey = '';
var portValue = '';
if (key === '') {
icon = <td><a disabled={isUpdating} onClick={this.handleAddPort} className="only-icon btn btn-positive small"><span className="icon icon-add"></span></a></td>;
portKey = <input id={'portKey' + key} type="text" disabled={isUpdating} defaultValue={key} />;
portValue = <input id={'portValue' + key} type="text" disabled={isUpdating} defaultValue={port} />;
}else {
if (this.isInitialPort(key, this.state.initialPorts)) {
icon = <td></td>;
}else {
icon = <td><a disabled={isUpdating} onClick={this.handleRemovePort.bind(this, key)} className="only-icon btn btn-action small"><span className="icon icon-delete"></span></a></td>;
}
portKey = <input id={'portKey' + key} type="text" onChange={this.handleChangePortKey.bind(this, key)} disabled={isUpdating} defaultValue={key} />;
portValue = <input id={'portValue' + key} type="text" onChange={this.handleChangePort.bind(this, key)} disabled={isUpdating} defaultValue={port} />;
}
return (
<tr key={key}>
<td>{portKey}</td>
<td className="bind">
{ipLink}:
{portValue}
</td>
<td>
<DropdownButton disabled={isUpdating} id= {'portType' + key } bsStyle="primary" title={portType} >
<MenuItem onSelect={this.handleChangePortType.bind(this, key, 'tcp')} key={key + '-tcp'}>TCP</MenuItem>
<MenuItem onSelect={this.handleChangePortType.bind(this, key, 'udp')} key={key + '-udp'}>UDP</MenuItem>
</DropdownButton>
</td>
{icon}
<td className="error">{error}</td>
</tr>
);
});
return (
<div className="settings-panel">
<div className="settings-section">
<h3>Configure Hostname</h3>
<div className="container-info-row">
<div className="label-hostname">HOSTNAME</div>
<input id="hostname" className="line" type="text" disabled={isUpdating} value={this.state.hostname} onChange={this.handleChangeHostnameEnabled}/>
</div>
</div>
<div className="settings-section">
<h3>Configure Ports</h3>
<table className="table ports">
<thead>
<tr>
<th>DOCKER PORT</th>
<th>PUBLISHED IP:PORT</th>
<th></th>
</tr>
</thead>
<tbody>
{ports}
</tbody>
</table>
<a className="btn btn-action"
disabled={isUpdating || !isValid}
onClick={this.handleSave}>
Save
</a>
</div>
</div>
);
}
});
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 = (
<span className="value-right">No Folder</span>
);
} else {
let local = util.isWindows() ? util.linuxToWindowsPath(source) : source;
source = (
<a className="value-right" onClick={this.handleOpenVolumeClick.bind(this, source)}>{local.replace(process.env.HOME, '~')}</a>
);
}
return (
<tr>
<td>{destination}</td>
<td>{source}</td>
<td>
<a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleChooseVolumeClick.bind(this, destination)}>Change</a>
<a className="btn btn-action small" disabled={this.props.container.State.Updating} onClick={this.handleRemoveVolumeClick.bind(this, destination)}>Remove</a>
</td>
</tr>
);
});
return (
<div className="settings-panel">
<div className="settings-section">
<h3>Configure Volumes</h3>
<table className="table volumes">
<thead>
<tr>
<th>DOCKER FOLDER</th>
<th>LOCAL FOLDER</th>
<th></th>
</tr>
</thead>
<tbody>
{mounts}
</tbody>
</table>
</div>
</div>
);
}
});
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 (
<div className="containers">
<Header />
<div className="containers-body">
<div className="sidebar">
<section className={sidebarHeaderClass}>
<h4>Containers</h4>
<div className="create">
<Router.Link to="search">
<span className="btn btn-new btn-action has-icon btn-hollow"><span className="icon icon-add"></span>New</span>
</Router.Link>
</div>
</section>
<section className="sidebar-containers" onScroll={this.handleScroll}>
<ContainerList containers={this.state.sorted} newContainer={this.state.newContainer} />
</section>
<section className="sidebar-buttons">
<span className="btn-sidebar btn-terminal" onClick={this.handleClickDockerTerminal} ><span className="icon icon-docker-cli"></span><span className="text">DOCKER CLI</span></span>
<span className="btn-sidebar btn-feedback" onClick={this.handleClickReportIssue} ><span className="icon icon-feedback"></span></span>
<span className="btn-sidebar btn-preferences" onClick={this.handleClickPreferences} ><span className="icon icon-preferences"></span></span>
</section>
</div>
<Router.RouteHandler pending={this.state.pending} containers={this.state.containers} container={container}/>
</div>
</div>
);
}
});
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 (
<div className="logo">
<RetinaImage src="logo.png"/>
</div>
);
},
renderWindowButtons: function () {
let buttons;
if (util.isWindows()) {
buttons = (
<div className="windows-buttons">
<div className="windows-button button-minimize enabled" onClick={this.handleMinimize}><div className="icon"></div></div>
<div className={`windows-button ${this.state.fullscreen ? 'button-fullscreenclose' : 'button-fullscreen'} enabled`} onClick={this.handleFullscreen}><div className="icon"></div></div>
<div className="windows-button button-close enabled" onClick={this.handleClose}></div>
</div>
);
} else {
buttons = (
<div className="buttons">
<div className="button button-close enabled" onClick={this.handleClose}></div>
<div className="button button-minimize enabled" onClick={this.handleMinimize}></div>
<div className="button button-fullscreen enabled" onClick={this.handleFullscreen}></div>
</div>
);
}
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 = (
<div className="login-wrapper">
<div className="login no-drag" onClick={this.handleUserClick}>
<span className="icon icon-user"></span>
<span className="text">
{this.state.username}
{this.state.verified ? null : '(Unverified)'}
</span>
<RetinaImage src="userdropdown.png"/>
</div>
</div>
);
} else {
username = (
<div className="login-wrapper">
<div className="login no-drag" onClick={this.handleLoginClick}>
<span className="icon icon-user"></span> LOGIN
</div>
</div>
);
}
return (
<div className={headerClasses}>
<div className="left-header">
{util.isWindows () ? this.renderLogo() : this.renderWindowButtons()}
{username}
</div>
<div className="right-header">
{util.isWindows () ? this.renderWindowButtons() : this.renderLogo()}
</div>
</div>
);
},
renderBasicHeader: function () {
let headerClasses = classNames({
bordered: !this.props.hideLogin,
header: true,
'no-drag': true
});
return (
<div className={headerClasses}>
<div className="left-header">
{util.isWindows () ? null : this.renderWindowButtons()}
</div>
<div className="right-header">
{util.isWindows () ? this.renderWindowButtons() : null}
</div>
</div>
);
},
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 = (
<div>
<div className="namespace official">official</div>
<span className="repo">{this.props.image.name}</span>
</div>
);
} else {
name = (
<div>
<div className="namespace">{this.props.image.namespace}</div>
<span className="repo">{this.props.image.name}</span>
</div>
);
}
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 = <RetinaImage className="items-loading" src="loading.png"/>;
} else if (this.state.tags.length === 0) {
tags = <div className="no-items">No Tags</div>;
} 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 <div className="item active" key={key} onClick={this.handleTagClick.bind(this, t)}>{t}</div>;
} else {
return <div className="item" key={key} onClick={this.handleTagClick.bind(this, t)}>{t}</div>;
}
});
tags = (
<div className="item-list tag-list">
{tagDisplay}
</div>
);
}
let networkDisplay = this.state.networks.map((network) => {
let networkName = network.Name;
if (networkName === this.state.defaultNetwork) {
return <div className="item active" key={networkName} onClick={this.handleNetworkClick.bind(this, networkName)}>{networkName}</div>;
} else {
return <div className="item" key={networkName} onClick={this.handleNetworkClick.bind(this, networkName)}>{networkName}</div>;
}
});
let networks = (
<div className="item-list network-list">
{networkDisplay}
</div>
);
var badge = null;
if (this.props.image.namespace === 'library') {
badge = (
<span className="icon icon-badge-official"></span>
);
} else if (this.props.image.is_private) {
badge = (
<span className="icon icon-badge-private"></span>
);
}
let create, overlay;
if (this.props.image.is_local) {
create = (
<div className="actions">
<div className="favorites">
<span className="icon icon-tag"> {this.state.chosenTag}</span>
<span className="text"></span>
</div>
<div className="more-menu" onClick={this.handleMenuOverlayClick}>
<span className="icon icon-more"></span>
</div>
<div className="action" onClick={this.handleClick}>
CREATE
</div>
</div>
);
overlay = (
<div className="overlay menu-overlay">
<div className="menu-item" onClick={this.handleTagOverlayClick.bind(this, this.props.image.name)}>
<span className="icon icon-tag"></span><span className="text">SELECTED TAG: <span className="selected-item">{this.state.chosenTag}</span></span>
</div>
<div className="remove" onClick={this.handleDeleteImgClick.bind(this, this.props.image)}>
<span className="btn btn-delete btn-action has-icon btn-hollow" disabled={this.props.image.inUse ? 'disabled' : null}><span className="icon icon-delete"></span>Delete Tag</span>
</div>
{this.props.image.inUse ? <p className="small">To delete, remove all containers<br/>using the above image</p> : null }
<div className="close-overlay">
<a className="btn btn-action circular" onClick={this.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a>
</div>
</div>
);
} 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 = (
<div className="actions">
<div className="favorites">
<span className="icon icon-favorite"></span>
<span className="text">{favCount}</span>
<span className="icon icon-download"></span>
<span className="text">{pullCount}</span>
</div>
<div className="more-menu" onClick={this.handleMenuOverlayClick}>
<span className="icon icon-more"></span>
</div>
<div className="action" onClick={this.handleClick}>
CREATE
</div>
</div>
);
overlay = (
<div className="overlay menu-overlay">
<div className="menu-item" onClick={this.handleTagOverlayClick.bind(this, this.props.image.name)}>
<span className="icon icon-tag"></span><span className="text">SELECTED TAG: <span className="selected-item">{this.state.chosenTag}</span></span>
</div>
<div className="menu-item" onClick={this.handleNetworkOverlayClick.bind(this, this.props.image.name)}>
<span className="icon icon-link"></span><span className="text">DEFAULT NETWORK: <span className="selected-item">{this.state.defaultNetwork}</span></span>
</div>
<div className="menu-item" onClick={this.handleRepoClick}>
<span className="icon icon-open-external"></span><span className="text">VIEW ON DOCKER HUB</span>
</div>
<div className="close-overlay">
<a className="btn btn-action circular" onClick={this.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a>
</div>
</div>
);
}
let searchTagInputStyle = { outline: 'none', width: 'calc(100% - 30px)' };
return (
<div className="image-item">
{overlay}
<div className="overlay item-overlay tag-overlay">
<p>
<input
ref="searchTagInput"
style={searchTagInputStyle}
type="text"
placeholder="Filter image tag."
onChange={this.searchTag}
/>
</p>
{tags}
<div className="close-overlay" onClick={this.handleCloseTagOverlay}>
<a className="btn btn-action circular"><span className="icon icon-delete"></span></a>
</div>
</div>
<div className="overlay item-overlay network-overlay">
<p>Please select an default network.</p>
{networks}
<div className="close-overlay" onClick={this.handleCloseNetworkOverlay}>
<a className="btn btn-action circular"><span className="icon icon-delete"></span></a>
</div>
</div>
<div className="logo" style={logoStyle}>
<RetinaImage src={imgsrc}/>
</div>
<div className="card">
<div className="info">
<div className="badges">
{badge}
</div>
<div className="name">
{name}
</div>
<div className="description">
{description}
</div>
</div>
{create}
</div>
</div>
);
}
});
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 (
<div className="loading">
<Header hideLogin={true}/>
<div className="loading-content">
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
</div>
);
}
});
================================================
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((
<li>
<a href="" onClick={this.handlePage.bind(this, 1)} aria-label="First">
<span aria-hidden="true">«</span>
</a>
</li>
));
for (previousPage; previousPage < this.state.currentPage; previousPage++) {
previous.push((
<li><a href="" onClick={this.handlePage.bind(this, previousPage)}>{previousPage}</a></li>
));
}
}
if (this.state.nextPage) {
let nextPage = this.state.currentPage + 1;
for (nextPage; nextPage < this.state.totalPage; nextPage++) {
next.push((
<li><a href="" onClick={this.handlePage.bind(this, nextPage)}>{nextPage}</a></li>
));
if (nextPage > this.state.currentPage + 7) {
break;
}
}
next.push((
<li>
<a href="" onClick={this.handlePage.bind(this, this.state.totalPage)} aria-label="Last">
<span aria-hidden="true">»</span>
</a>
</li>
));
}
let current = (
<li className="active">
<span>{this.state.currentPage} <span className="sr-only">(current)</span></span>
</li>
);
paginateResults = (next.length || previous.length) && (this.state.query !== '') ? (
<nav>
<ul className="pagination">
{previous}
{current}
{next}
</ul>
</nav>
) : 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 = (
<div className="no-results">
<h2 className="error">{errorMsg}</h2>
</div>
);
paginateResults = null;
} else if (filter === 'userrepos' && !accountStore.getState().username) {
results = (
<div className="no-results">
<h2><Router.Link to="login">Log In</Router.Link> or <Router.Link to="signup">Sign Up</Router.Link> to access your Docker Hub repositories.</h2>
<RetinaImage src="connect-art.png" checkIfRetinaImgExists={false}/>
</div>
);
paginateResults = null;
} else if (filter === 'userrepos' && !accountStore.getState().verified) {
let spinner = this.state.accountLoading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
results = (
<div className="no-results">
<h2>Please verify your Docker Hub account email address</h2>
<div className="verify">
<button className="btn btn-action" onClick={this.handleCheckVerification}>{'I\'ve Verified My Email Address'}</button> {spinner}
</div>
<RetinaImage src="inspection.png" checkIfRetinaImgExists={false}/>
</div>
);
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 !== '<none>');
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 (
<ImageCard key={key + ':' + tags} image={image} chosenTag={image.tags[0]} tags={image.tags} />
);
});
let userImageResults = userImageItems.length ? (
<div className="result-grids">
<div>
<h4>My Images</h4>
<div className="result-grid">
{userImageItems}
</div>
</div>
</div>
) : (
<div className="no-results">
<h2>Cannot find any local image.</h2>
</div>
);
results = (
{userImageResults}
);
paginateResults = null;
} else if (this.state.loading) {
results = (
<div className="no-results">
<div className="loader">
<h2>Loading Images</h2>
<div className="spinner la-ball-clip-rotate la-dark la-lg"><div></div></div>
</div>
</div>
);
} else if (repos.length) {
let recommendedItems = repos.filter(repo => repo.is_recommended).map((image, index) => {
const key = `rec-${image.name}-${index}`;
return (<ImageCard key={key} image={image} />);
});
let otherItems = repos.filter(repo => !repo.is_recommended && !repo.is_user_repo).map((image, index) => {
const key = `other-${image.name}-${index}`;
return (<ImageCard key={key} image={image} />);
});
let recommendedResults = recommendedItems.length ? (
<div>
<h4>Recommended</h4>
<div className="result-grid">
{recommendedItems}
</div>
</div>
) : null;
let userRepoItems = repos.filter(repo => repo.is_user_repo).map((image, index) => {
const key = `usr-${image.name}-${index}`;
return (<ImageCard key={key} image={image} />);
});
let userRepoResults = userRepoItems.length ? (
<div>
<h4>My Repositories</h4>
<div className="result-grid">
{userRepoItems}
</div>
</div>
) : null;
let otherResults;
if (otherItems.length) {
otherResults = (
<div>
<h4>Other Repositories</h4>
<div className="result-grid">
{otherItems}
</div>
</div>
);
} else {
otherResults = null;
paginateResults = null;
}
results = (
<div className="result-grids">
{recommendedResults}
{userRepoResults}
{otherResults}
</div>
);
} else {
if (this.state.query.length) {
results = (
<div className="no-results">
<h2>Cannot find a matching image.</h2>
</div>
);
} else {
results = (
<div className="no-results">
<h2>No Images</h2>
</div>
);
}
}
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 (
<div className="details">
<div className="new-container">
<div className="new-container-header">
<div className="search">
<div className={searchClasses}>
<input type="search" ref="searchInput" className="form-control" placeholder="Search for Docker images from Docker Hub" onChange={this.handleChange}/>
<div className={magnifierClasses}></div>
<div className={loadingClasses}><div></div></div>
</div>
</div>
<div className="results-filters">
<span className="results-filter results-filter-title">FILTER BY</span>
<span className={`results-filter results-all tab ${filter === 'all' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'all')}>All</span>
<span className={`results-filter results-recommended tab ${filter === 'recommended' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'recommended')}>Recommended</span>
<span className={`results-filter results-userrepos tab ${filter === 'userrepos' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userrepos')}>My Repos</span>
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
SYMBOL INDEX (161 symbols across 26 files)
FILE: src/actions/AccountActions.js
class AccountActions (line 4) | class AccountActions {
method login (line 5) | login (username, password) {
method signup (line 10) | signup (username, password, email, subscribe) {
method logout (line 15) | logout () {
method skip (line 20) | skip () {
method verify (line 25) | verify () {
FILE: src/actions/AccountServerActions.js
class AccountServerActions (line 3) | class AccountServerActions {
method constructor (line 4) | constructor () {
FILE: src/actions/ContainerActions.js
class ContainerActions (line 5) | class ContainerActions {
method destroy (line 7) | destroy (name) {
method rename (line 11) | rename (name, newName) {
method start (line 16) | start (name) {
method stop (line 21) | stop (name) {
method restart (line 25) | restart (name) {
method update (line 30) | update (name, container) {
method clearPending (line 35) | clearPending () {
method run (line 39) | run (name, repo, tag, network, local=false) {
method active (line 43) | active (name) {
method toggleFavorite (line 47) | toggleFavorite (name) {
FILE: src/actions/ContainerServerActions.js
class ContainerServerActions (line 3) | class ContainerServerActions {
method constructor (line 4) | constructor () {
FILE: src/actions/ImageActions.js
class ImageActions (line 4) | class ImageActions {
method all (line 6) | all () {
method destroy (line 11) | destroy (image) {
FILE: src/actions/ImageServerActions.js
class ImageServerActions (line 3) | class ImageServerActions {
method constructor (line 4) | constructor () {
FILE: src/actions/NetworkActions.js
class NetworkActions (line 3) | class NetworkActions {
method constructor (line 4) | constructor () {
FILE: src/actions/RepositoryActions.js
class RepositoryActions (line 4) | class RepositoryActions {
method recommended (line 5) | recommended () {
method search (line 10) | search (query, page = 1) {
method repos (line 15) | repos () {
FILE: src/actions/RepositoryServerActions.js
class RepositoryServerActions (line 3) | class RepositoryServerActions {
method constructor (line 4) | constructor () {
FILE: src/actions/SetupActions.js
class SetupActions (line 4) | class SetupActions {
method retry (line 5) | retry (removeVM) {
method useVbox (line 10) | useVbox () {
FILE: src/actions/SetupServerActions.js
class SetupServerActions (line 3) | class SetupServerActions {
method constructor (line 4) | constructor () {
FILE: src/actions/TagActions.js
class TagActions (line 4) | class TagActions {
method tags (line 5) | tags (repo) {
method localTags (line 10) | localTags (repo, tags) {
FILE: src/actions/TagServerActions.js
class TagServerActions (line 3) | class TagServerActions {
method constructor (line 4) | constructor () {
FILE: src/components/ContainerSettingsNetwork.react.js
method getUsedNetworks (line 36) | getUsedNetworks(networks) {
FILE: src/stores/AccountStore.js
class AccountStore (line 5) | class AccountStore {
method constructor (line 6) | constructor () {
method skip (line 18) | skip () {
method login (line 24) | login () {
method logout (line 31) | logout () {
method signup (line 40) | signup () {
method loggedin (line 47) | loggedin ({username, verified}) {
method loggedout (line 51) | loggedout () {
method signedup (line 60) | signedup ({username}) {
method verify (line 64) | verify () {
method verified (line 68) | verified ({verified}) {
method prompted (line 72) | prompted ({prompted}) {
method errors (line 76) | errors ({errors}) {
FILE: src/stores/ContainerStore.js
constant MAX_LOG_SIZE (line 6) | let MAX_LOG_SIZE = 3000;
class ContainerStore (line 8) | class ContainerStore {
method constructor (line 9) | constructor () {
method error (line 18) | error ({name, error}) {
method start (line 26) | start ({name}) {
method started (line 34) | started ({name}) {
method stopped (line 43) | stopped ({id}) {
method kill (line 53) | kill ({id}) {
method rename (line 63) | rename ({name, newName}) {
method added (line 77) | added ({container}) {
method update (line 83) | update ({name, container}) {
method updated (line 102) | updated ({container}) {
method allUpdated (line 120) | allUpdated ({containers}) {
method progress (line 126) | progress ({name, progress}) {
method destroyed (line 136) | destroyed ({id}) {
method waiting (line 150) | waiting ({name, waiting}) {
method pending (line 158) | pending ({repo, tag}) {
method clearPending (line 163) | clearPending () {
method log (line 167) | log ({name, entry}) {
method logs (line 182) | logs ({name, logs}) {
method toggleFavorite (line 194) | toggleFavorite ({name}) {
method generateName (line 204) | static generateName (repo) {
FILE: src/stores/ImageStore.js
class ImageStore (line 5) | class ImageStore {
method constructor (line 6) | constructor () {
method error (line 16) | error (error) {
method clearError (line 20) | clearError () {
method destroyed (line 24) | destroyed (data) {
method updated (line 32) | updated (images) {
method all (line 55) | static all () {
FILE: src/stores/NetworkStore.js
class NetworkStore (line 4) | class NetworkStore {
method constructor (line 5) | constructor () {
method error (line 12) | error (error) {
method updated (line 16) | updated (networks) {
method pending (line 20) | pending () {
method clearPending (line 24) | clearPending () {
method all (line 28) | static all () {
FILE: src/stores/RepositoryStore.js
class RepositoryStore (line 8) | class RepositoryStore {
method constructor (line 9) | constructor () {
method error (line 27) | error ({error}) {
method repos (line 31) | repos () {
method reposLoading (line 35) | reposLoading () {
method reposUpdated (line 39) | reposUpdated ({repos}) {
method search (line 49) | search ({query, page}) {
method resultsUpdated (line 59) | resultsUpdated ({repos, page, previous, next, total}) {
method recommended (line 63) | recommended () {
method recommendedUpdated (line 67) | recommendedUpdated ({repos}) {
method loggedout (line 71) | loggedout () {
method all (line 75) | static all () {
method loading (line 81) | static loading () {
FILE: src/stores/SetupStore.js
class SetupStore (line 5) | class SetupStore {
method constructor (line 6) | constructor () {
method started (line 14) | started ({started}) {
method error (line 18) | error ({error}) {
method progress (line 22) | progress ({progress}) {
FILE: src/stores/TagStore.js
class TagStore (line 6) | class TagStore {
method constructor (line 7) | constructor () {
method tags (line 19) | tags ({repo}) {
method localTags (line 24) | localTags ({repo, tags}) {
method tagsUpdated (line 33) | tagsUpdated ({repo, tags}) {
method remove (line 39) | remove ({repo}) {
method loggedout (line 45) | loggedout () {
method error (line 51) | error ({repo}) {
FILE: src/utils/DockerUtil.js
method setup (line 43) | setup (ip, name) {
method version (line 73) | async version () {
method init (line 95) | init () {
method isDockerRunning (line 126) | isDockerRunning () {
method startContainer (line 134) | startContainer (name) {
method createContainer (line 148) | createContainer (name, containerData) {
method fetchContainer (line 222) | fetchContainer (id) {
method fetchAllContainers (line 242) | fetchAllContainers () {
method fetchAllImages (line 285) | fetchAllImages () {
method fetchAllNetworks (line 320) | fetchAllNetworks () {
method updateContainerNetworks (line 339) | updateContainerNetworks(name, connectedNetworks, disconnectedNetworks) {
method addOrRemoveNetworks (line 353) | addOrRemoveNetworks(name, networks, connect) {
method removeImage (line 375) | removeImage (selectedRepoTag) {
method run (line 400) | run (name, repository, tag, network, local = false) {
method updateContainer (line 461) | updateContainer (name, data) {
method rename (line 490) | rename (name, newName) {
method restart (line 521) | restart (name) {
method stop (line 539) | stop (name) {
method start (line 550) | start (name, callback) {
method startLinkedContainers (line 595) | startLinkedContainers (name, callback){
method destroy (line 652) | destroy (name) {
method active (line 681) | active (name) {
method logs (line 690) | logs () {
method attach (line 715) | attach () {
method detachLog (line 752) | detachLog() {
method detachEvent (line 758) | detachEvent() {
method listen (line 766) | listen () {
method pullImage (line 818) | pullImage (repository, tag, callback, progressCallback, blockedCallback) {
method refresh (line 933) | refresh () {
FILE: src/utils/HubUtil.js
constant HUB2_ENDPOINT (line 6) | let HUB2_ENDPOINT = process.env.HUB2_ENDPOINT || 'https://hub.docker.com...
FILE: src/utils/RegHubUtil.js
constant REGHUB2_ENDPOINT (line 15) | let REGHUB2_ENDPOINT = process.env.REGHUB2_ENDPOINT || 'https://hub.dock...
constant PAGING (line 17) | let PAGING = 24;
FILE: src/utils/SetupUtil.js
method simulateProgress (line 23) | simulateProgress (estimateSeconds) {
method clearTimers (line 34) | clearTimers () {
method useVbox (line 39) | async useVbox () {
method retry (line 48) | retry (removeVM) {
method pause (line 64) | pause () {
method setup (line 69) | async setup () {
method nativeSetup (line 91) | async nativeSetup () {
method nonNativeSetup (line 106) | async nonNativeSetup () {
FILE: src/utils/Util.js
function isValidPart (line 149) | function isValidPart (x) {
Condensed preview — 194 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (610K chars).
[
{
"path": ".babelrc",
"chars": 125,
"preview": "{\n \"presets\": [\n \"env\",\n \"react\"\n ],\n \"plugins\": [\n \"transform-runtime\",\n \"transform-async-to-generator\"\n"
},
{
"path": ".circleci/config.yml",
"chars": 790,
"preview": "version: 2\njobs:\n test:\n macos:\n xcode: \"9.0\"\n \n steps:\n - run:\n name: Install node@10\n "
},
{
"path": ".eslintrc",
"chars": 2496,
"preview": "root: true\n\nplugins:\n - react\n\nparserOptions:\n ecmaVersion: 2017\n sourceType: module\n\nenv:\n node: true\n e"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 133,
"preview": "### Expected behavior\n\n### Actual behavior\n\n### Information about the Issue\n\n\n### Steps to reproduce the behavior\n\n 1. "
},
{
"path": ".gitignore",
"chars": 275,
"preview": ".DS_Store\n.swp\nbuild\ndist\ndist-electron-builder/\nrelease\nsrc/**/*.js.map\ninstaller\nnode_modules\ncoverage\nnpm-debug.log\n\n"
},
{
"path": ".travis.yml",
"chars": 156,
"preview": "sudo: false\n\nlanguage: node_js\nnode_js:\n - \"8\"\n - \"10\"\n\ncache:\n directories:\n - node_modules\n\nscript:\n - npm "
},
{
"path": "CONTRIBUTING.md",
"chars": 5672,
"preview": "# Contributing to Kitematic\n\nThanks for contributing and supporting the Kitematic project!\n\nBefore you file an issue or "
},
{
"path": "Gruntfile.js",
"chars": 11472,
"preview": "var packagejson = require('./package.json');\nvar electron = require('electron');\n\nmodule.exports = function (grunt) {\n "
},
{
"path": "LICENSE",
"chars": 11348,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "MAINTAINERS",
"chars": 1091,
"preview": "# Kitematic maintainers file\n#\n# This file describes who runs the docker/kitematic project and how.\n# This is a living d"
},
{
"path": "Makefile",
"chars": 1729,
"preview": ".PHONY: docs docs-shell docs-build run\n\nVERSION := $(shell jq -r '.version' package.json)\n\n# TODO: clearly need to note "
},
{
"path": "README.md",
"chars": 3686,
"preview": "### :warning: Deprecation Notice: This project and repository is now deprecated and is no longer under active developmen"
},
{
"path": "ROADMAP.md",
"chars": 862,
"preview": "## Kitematic Roadmap\n\n**January 2015**\n\n* Automatic updates\n* Stability bug fixes\n\n**Februay 2015**\n\n* Docker machine su"
},
{
"path": "__integration__/HubUtil-integration.js",
"chars": 956,
"preview": "jest.autoMockOff();\n\njasmine.getEnv().defaultTimeoutInterval = 60000;\n\nlet hubUtil = require('../src/utils/HubUtil');\nle"
},
{
"path": "__integration__/RegHubUtil-integration.js",
"chars": 2044,
"preview": "jest.autoMockOff();\n\njasmine.getEnv().defaultTimeoutInterval = 60000;\n\nlet _ = require('underscore');\nlet regHubUtil = r"
},
{
"path": "__mocks__/app.js",
"chars": 80,
"preview": "module.exports = {\n require: jest.fn(),\n match: jest.fn(),\n on: jest.fn()\n};\n"
},
{
"path": "__mocks__/electron.js",
"chars": 123,
"preview": "module.exports = {\n require: jest.fn(),\n match: jest.fn(),\n app: jest.fn(),\n remote: jest.fn(),\n dialog: jest.fn()\n"
},
{
"path": "__mocks__/remote.js",
"chars": 63,
"preview": "module.exports = {\n require: jest.fn(),\n match: jest.fn()\n};\n"
},
{
"path": "__tests__/Util-test.js",
"chars": 7136,
"preview": "jest.dontMock('../src/utils/Util').dontMock('console');\nconst util = require('../src/utils/Util');\n\ndescribe('Util', () "
},
{
"path": "docs/README.md",
"chars": 354,
"preview": "# The docs have been moved!\n\nThe documentation for Kitematic has been merged into\n[the general documentation repo](https"
},
{
"path": "electron-builder.json",
"chars": 639,
"preview": "{\n \"appId\": \"com.docker.kitematic\",\n \"asar\": true,\n \"directories\": {\n \"output\": \"./dist/\"\n },\n \"files\": [\n {\n"
},
{
"path": "index.html",
"chars": 328,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <link rel=\"stylesheet\" href=\"main.css\"/>\n <meta http-equiv=\"Content-Security-Poli"
},
{
"path": "jest-integration.json",
"chars": 335,
"preview": "{\n \"testMatch\": [\"**/__integration__/**/*.js\"],\n \"transform\": {\".*\": \"<rootDir>/node_modules/babel-jest\"},\n \"setupFil"
},
{
"path": "jest-unit.json",
"chars": 449,
"preview": "{\n \"transform\": { \".*\": \"<rootDir>/node_modules/babel-jest\" },\n \"setupFiles\": [\"<rootDir>/util/testenv.js\"],\n \"setupT"
},
{
"path": "package.json",
"chars": 3658,
"preview": "{\n \"name\": \"Kitematic\",\n \"version\": \"0.17.13\",\n \"author\": \"Kitematic\",\n \"license\": \"Apache-2.0\",\n \"description\": \"S"
},
{
"path": "resources/MSYS_LICENSE",
"chars": 1049,
"preview": "Kitematic includes (but does not link to) various DLLs included with the msysgit Git-1.9.5-preview20150319 distribution."
},
{
"path": "resources/OPENSSH_LICENSE",
"chars": 10187,
"preview": "Kitematic includes OpenSSH ssh.exe from the msysgit distribution version Git-1.9.5-preview20150319, available online at "
},
{
"path": "resources/terminal",
"chars": 1491,
"preview": "#!/bin/bash\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nCMD=\"clear && $*\"\n\nITERM_EXISTS=`osascript <<EOF\nse"
},
{
"path": "src/actions/AccountActions.js",
"chars": 551,
"preview": "import alt from '../alt';\nimport hub from '../utils/HubUtil';\n\nclass AccountActions {\n login (username, password) {\n "
},
{
"path": "src/actions/AccountServerActions.js",
"chars": 277,
"preview": "import alt from '../alt';\n\nclass AccountServerActions {\n constructor () {\n this.generateActions(\n 'signedup',\n "
},
{
"path": "src/actions/ContainerActions.js",
"chars": 1261,
"preview": "import alt from '../alt';\nimport dockerUtil from '../utils/DockerUtil';\nimport _ from \"underscore\";\n\nclass ContainerActi"
},
{
"path": "src/actions/ContainerServerActions.js",
"chars": 443,
"preview": "import alt from '../alt';\n\nclass ContainerServerActions {\n constructor () {\n this.generateActions(\n 'added',\n "
},
{
"path": "src/actions/ImageActions.js",
"chars": 270,
"preview": "import alt from '../alt';\nimport dockerUtil from '../utils/DockerUtil';\n\nclass ImageActions {\n\n all () {\n this.dispa"
},
{
"path": "src/actions/ImageServerActions.js",
"chars": 232,
"preview": "import alt from '../alt';\n\nclass ImageServerActions {\n constructor () {\n this.generateActions(\n 'added',\n "
},
{
"path": "src/actions/NetworkActions.js",
"chars": 229,
"preview": "import alt from '../alt';\n\nclass NetworkActions {\n constructor () {\n this.generateActions(\n 'updated',\n 'e"
},
{
"path": "src/actions/RepositoryActions.js",
"chars": 400,
"preview": "import alt from '../alt';\nimport regHubUtil from '../utils/RegHubUtil';\n\nclass RepositoryActions {\n recommended () {\n "
},
{
"path": "src/actions/RepositoryServerActions.js",
"chars": 287,
"preview": "import alt from '../alt';\n\nclass RepositoryServerActions {\n constructor () {\n this.generateActions(\n 'reposLoad"
},
{
"path": "src/actions/SetupActions.js",
"chars": 298,
"preview": "import alt from '../alt';\nimport setupUtil from '../utils/SetupUtil';\n\nclass SetupActions {\n retry (removeVM) {\n thi"
},
{
"path": "src/actions/SetupServerActions.js",
"chars": 216,
"preview": "import alt from '../alt';\n\nclass SetupServerActions {\n constructor () {\n this.generateActions(\n 'progress',\n "
},
{
"path": "src/actions/TagActions.js",
"chars": 282,
"preview": "import alt from '../alt';\nimport regHubUtil from '../utils/RegHubUtil';\n\nclass TagActions {\n tags (repo) {\n this.dis"
},
{
"path": "src/actions/TagServerActions.js",
"chars": 198,
"preview": "import alt from '../alt';\n\nclass TagServerActions {\n constructor () {\n this.generateActions(\n 'tagsUpdated',\n "
},
{
"path": "src/alt.js",
"chars": 49,
"preview": "import Alt from 'alt';\nexport default new Alt();\n"
},
{
"path": "src/app.js",
"chars": 1965,
"preview": "import 'babel-polyfill';\nimport electron from 'electron';\nconst remote = electron.remote;\nconst Menu = remote.Menu;\n// i"
},
{
"path": "src/browser.js",
"chars": 2441,
"preview": "import electron from 'electron';\nconst app = electron.app;\nconst BrowserWindow = electron.BrowserWindow;\n\nimport fs from"
},
{
"path": "src/components/About.react.js",
"chars": 1921,
"preview": "import React from 'react/addons';\nimport metrics from '../utils/MetricsUtil';\nimport utils from '../utils/Util';\nimport "
},
{
"path": "src/components/Account.react.js",
"chars": 2286,
"preview": "import React from 'react/addons';\nimport Router from 'react-router';\nimport RetinaImage from 'react-retina-image';\nimpor"
},
{
"path": "src/components/AccountLogin.react.js",
"chars": 3145,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport validator from '"
},
{
"path": "src/components/AccountSignup.react.js",
"chars": 3425,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport validator from '"
},
{
"path": "src/components/ContainerDetails.react.js",
"chars": 1062,
"preview": "import React from 'react/addons';\nimport Router from 'react-router';\nimport ContainerDetailsHeader from './ContainerDeta"
},
{
"path": "src/components/ContainerDetailsHeader.react.js",
"chars": 1351,
"preview": "import React from 'react/addons';\n\nvar ContainerDetailsHeader = React.createClass({\n render: function () {\n var stat"
},
{
"path": "src/components/ContainerDetailsSubheader.react.js",
"chars": 6669,
"preview": "import _ from 'underscore';\nimport React from 'react';\nimport {shell} from 'electron';\nimport metrics from '../utils/Met"
},
{
"path": "src/components/ContainerHome.react.js",
"chars": 4789,
"preview": "import _ from 'underscore';\nimport $ from 'jquery';\nimport React from 'react/addons';\nimport ContainerProgress from './C"
},
{
"path": "src/components/ContainerHomeFolders.react.js",
"chars": 3202,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport RetinaImage from 'react-retina-image';\nimport path "
},
{
"path": "src/components/ContainerHomeIpPortsPreview.react.js",
"chars": 1292,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\n\nvar ContainerHomeIpPortsPreview = React.createClass({\n h"
},
{
"path": "src/components/ContainerHomeLogs.react.js",
"chars": 4310,
"preview": "import $ from 'jquery';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport containerActions fro"
},
{
"path": "src/components/ContainerList.react.js",
"chars": 517,
"preview": "import React from 'react/addons';\nimport ContainerListItem from './ContainerListItem.react';\n\nvar ContainerList = React."
},
{
"path": "src/components/ContainerListItem.react.js",
"chars": 4038,
"preview": "import $ from 'jquery';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport electron from 'elect"
},
{
"path": "src/components/ContainerProgress.react.js",
"chars": 1025,
"preview": "import React from 'react';\n\n/*\n\n Usage: <ContainerProgress pBar1={20} pBar2={70} pBar3={100} pBar4={20} />\n\n*/\nvar Cont"
},
{
"path": "src/components/ContainerSettings.react.js",
"chars": 2348,
"preview": "import $ from 'jquery';\nimport _ from 'underscore';\nimport React from 'react/addons';\nimport Router from 'react-router';"
},
{
"path": "src/components/ContainerSettingsAdvanced.react.js",
"chars": 2849,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport metrics from '../utils/MetricsUtil';\nimport Contain"
},
{
"path": "src/components/ContainerSettingsGeneral.react.js",
"chars": 7494,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport metrics from '../utils/MetricsUtil';\nimport electro"
},
{
"path": "src/components/ContainerSettingsNetwork.react.js",
"chars": 9231,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport metrics from '../utils/MetricsUtil';\nimport docker "
},
{
"path": "src/components/ContainerSettingsPorts.react.js",
"chars": 9397,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport {shell} from 'electron';\nimport ContainerUtil from "
},
{
"path": "src/components/ContainerSettingsVolumes.react.js",
"chars": 4183,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport electron from 'electron';\nconst remote = electron.r"
},
{
"path": "src/components/Containers.react.js",
"chars": 5005,
"preview": "import $ from 'jquery';\nimport _ from 'underscore';\nimport React from 'react';\nimport Router from 'react-router';\nimport"
},
{
"path": "src/components/Header.react.js",
"chars": 6179,
"preview": "import React from 'react/addons';\nimport RetinaImage from 'react-retina-image';\nimport util from '../utils/Util';\nimport"
},
{
"path": "src/components/ImageCard.react.js",
"chars": 13170,
"preview": "import $ from 'jquery';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport {shell} from 'electr"
},
{
"path": "src/components/Loading.react.js",
"chars": 385,
"preview": "import React from 'react/addons';\nimport Header from './Header.react';\n\nmodule.exports = React.createClass({\n render: f"
},
{
"path": "src/components/NewContainerSearch.react.js",
"chars": 13720,
"preview": "import _ from 'underscore';\nimport React from 'react/addons';\nimport Router from 'react-router';\nimport RetinaImage from"
},
{
"path": "src/components/Preferences.react.js",
"chars": 5803,
"preview": "import React from 'react/addons';\nimport metrics from '../utils/MetricsUtil';\nimport Router from 'react-router';\nimport "
},
{
"path": "src/components/Radial.react.js",
"chars": 1225,
"preview": "import React from 'react';\nimport classNames from 'classnames';\n\nvar Radial = React.createClass({\n render: function () "
},
{
"path": "src/components/Setup.react.js",
"chars": 5647,
"preview": "import React from 'react/addons';\nimport Router from 'react-router';\nimport Radial from './Radial.react.js';\nimport Reti"
},
{
"path": "src/main.js",
"chars": 48,
"preview": "import \"./app\";\n//# sourceMappingURL=main.js.map"
},
{
"path": "src/main.ts",
"chars": 16,
"preview": "import \"./app\";\n"
},
{
"path": "src/menutemplate.js",
"chars": 4813,
"preview": "import electron from 'electron';\nconst remote = electron.remote;\nimport {shell} from 'electron';\nimport router from './r"
},
{
"path": "src/router.js",
"chars": 147,
"preview": "module.exports = {\n router: null,\n\n get: function () {\n return this.router;\n },\n\n set: function (router) {\n th"
},
{
"path": "src/routes.js",
"chars": 2838,
"preview": "import React from 'react/addons';\nimport Setup from './components/Setup.react';\nimport Account from './components/Accoun"
},
{
"path": "src/stores/AccountStore.js",
"chars": 1423,
"preview": "import alt from '../alt';\nimport accountServerActions from '../actions/AccountServerActions';\nimport accountActions from"
},
{
"path": "src/stores/ContainerStore.js",
"chars": 5173,
"preview": "import _ from 'underscore';\nimport alt from '../alt';\nimport containerServerActions from '../actions/ContainerServerActi"
},
{
"path": "src/stores/ImageStore.js",
"chars": 1540,
"preview": "import alt from '../alt';\nimport imageActions from '../actions/ImageActions';\nimport imageServerActions from '../actions"
},
{
"path": "src/stores/NetworkStore.js",
"chars": 626,
"preview": "import alt from '../alt';\nimport networkActions from '../actions/NetworkActions';\n\nclass NetworkStore {\n constructor ()"
},
{
"path": "src/stores/RepositoryStore.js",
"chars": 2699,
"preview": "import _ from 'underscore';\nimport alt from '../alt';\nimport repositoryServerActions from '../actions/RepositoryServerAc"
},
{
"path": "src/stores/SetupStore.js",
"chars": 588,
"preview": "import alt from '../alt';\nimport setupServerActions from '../actions/SetupServerActions';\nimport setupActions from '../a"
},
{
"path": "src/stores/TagStore.js",
"chars": 1213,
"preview": "import alt from '../alt';\nimport tagActions from '../actions/TagActions';\nimport tagServerActions from '../actions/TagSe"
},
{
"path": "src/utils/ContainerUtil.js",
"chars": 2263,
"preview": "import _ from 'underscore';\nimport docker from '../utils/DockerUtil';\n\nvar ContainerUtil = {\n env: function (container)"
},
{
"path": "src/utils/DockerMachineUtil.js",
"chars": 6901,
"preview": "import _ from 'underscore';\nimport path from 'path';\nimport Promise from 'bluebird';\nimport fs from 'fs';\nimport util fr"
},
{
"path": "src/utils/DockerUtil.js",
"chars": 28572,
"preview": "import async from 'async';\nimport fs from 'fs';\nimport path from 'path';\nimport dockerode from 'dockerode';\nimport _ fro"
},
{
"path": "src/utils/HubUtil.js",
"chars": 6192,
"preview": "import _ from 'underscore';\nimport request from 'request';\nimport accountServerActions from '../actions/AccountServerAct"
},
{
"path": "src/utils/MetricsUtil.js",
"chars": 1583,
"preview": "import assign from 'object-assign';\nimport Mixpanel from 'mixpanel';\nimport uuid from 'node-uuid';\nimport fs from 'fs';\n"
},
{
"path": "src/utils/RegHubUtil.js",
"chars": 7364,
"preview": "import {isNullOrUndefined} from 'util';\nimport _ from 'underscore';\nimport request from 'request';\nimport async from 'as"
},
{
"path": "src/utils/SetupUtil.js",
"chars": 6726,
"preview": "import _ from 'underscore';\nimport fs from 'fs';\nimport path from 'path';\nimport Promise from 'bluebird';\nimport bugsnag"
},
{
"path": "src/utils/Util.js",
"chars": 6235,
"preview": "import child_process from 'child_process';\nimport Promise from 'bluebird';\nimport fs from 'fs';\nimport path from 'path';"
},
{
"path": "src/utils/VirtualBoxUtil.js",
"chars": 1596,
"preview": "import fs from 'fs';\nimport path from 'path';\nimport util from './Util';\nimport Promise from 'bluebird';\n\nvar VirtualBox"
},
{
"path": "src/utils/WebUtil.js",
"chars": 2522,
"preview": "import electron from 'electron';\nconst remote = electron.remote;\nconst app = remote.app;\nimport fs from 'fs';\nimport uti"
},
{
"path": "styles/animation.less",
"chars": 674,
"preview": "@-webkit-keyframes spin {\n from {\n -webkit-transform: rotate(0deg);\n }\n to {\n -webkit-transform: rotate(360deg)"
},
{
"path": "styles/bootstrap/alerts.less",
"chars": 1513,
"preview": "//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert"
},
{
"path": "styles/bootstrap/badges.less",
"chars": 1171,
"preview": "//\n// Badges\n// --------------------------------------------------\n\n\n// Base class\n.badge {\n display: inline-block;\n m"
},
{
"path": "styles/bootstrap/bootstrap.less",
"chars": 1121,
"preview": "// Core variables and mixins\n@import \"variables.less\";\n@import \"mixins.less\";\n\n// Reset and dependencies\n@import \"normal"
},
{
"path": "styles/bootstrap/breadcrumbs.less",
"chars": 594,
"preview": "//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n padding: @breadcrumb-padding-v"
},
{
"path": "styles/bootstrap/button-groups.less",
"chars": 5624,
"preview": "//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-gro"
},
{
"path": "styles/bootstrap/buttons.less",
"chars": 3552,
"preview": "//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------"
},
{
"path": "styles/bootstrap/carousel.less",
"chars": 5318,
"preview": "//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators"
},
{
"path": "styles/bootstrap/close.less",
"chars": 683,
"preview": "//\n// Close icons\n// --------------------------------------------------\n\n\n.close {\n float: right;\n font-size: (@font-s"
},
{
"path": "styles/bootstrap/code.less",
"chars": 1401,
"preview": "//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\nco"
},
{
"path": "styles/bootstrap/component-animations.less",
"chars": 709,
"preview": "//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `."
},
{
"path": "styles/bootstrap/dropdowns.less",
"chars": 4754,
"preview": "//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n display:"
},
{
"path": "styles/bootstrap/forms.less",
"chars": 13847,
"preview": "//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline"
},
{
"path": "styles/bootstrap/glyphicons.less",
"chars": 14879,
"preview": "//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus a"
},
{
"path": "styles/bootstrap/grid.less",
"chars": 1387,
"preview": "//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container wi"
},
{
"path": "styles/bootstrap/input-groups.less",
"chars": 4215,
"preview": "//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.i"
},
{
"path": "styles/bootstrap/jumbotron.less",
"chars": 983,
"preview": "//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n padding: @jumbotron-padding (@jum"
},
{
"path": "styles/bootstrap/labels.less",
"chars": 1079,
"preview": "//\n// Labels\n// --------------------------------------------------\n\n.label {\n display: inline;\n padding: .2em .6em .3e"
},
{
"path": "styles/bootstrap/list-group.less",
"chars": 3022,
"preview": "//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on <ul>, <ol"
},
{
"path": "styles/bootstrap/media.less",
"chars": 652,
"preview": ".media {\n // Proper spacing between instances of .media\n margin-top: 15px;\n\n &:first-child {\n margin-top: 0;\n }\n}"
},
{
"path": "styles/bootstrap/mixins/alerts.less",
"chars": 257,
"preview": "// Alerts\n\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;"
},
{
"path": "styles/bootstrap/mixins/background-variant.less",
"chars": 139,
"preview": "// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@"
},
{
"path": "styles/bootstrap/mixins/border-radius.less",
"chars": 468,
"preview": "// Single side border-radius\n\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radiu"
},
{
"path": "styles/bootstrap/mixins/buttons.less",
"chars": 1080,
"preview": "// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for"
},
{
"path": "styles/bootstrap/mixins/center-block.less",
"chars": 120,
"preview": "// Center-align a block level element\n\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n"
},
{
"path": "styles/bootstrap/mixins/clearfix.less",
"chars": 605,
"preview": "// Clearfix\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contentedi"
},
{
"path": "styles/bootstrap/mixins/forms.less",
"chars": 2641,
"preview": "// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and succ"
},
{
"path": "styles/bootstrap/mixins/gradients.less",
"chars": 4388,
"preview": "// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end"
},
{
"path": "styles/bootstrap/mixins/grid-framework.less",
"chars": 2784,
"preview": "// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any va"
},
{
"path": "styles/bootstrap/mixins/grid.less",
"chars": 3094,
"preview": "// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@"
},
{
"path": "styles/bootstrap/mixins/hide-text.less",
"chars": 579,
"preview": "// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins be"
},
{
"path": "styles/bootstrap/mixins/image.less",
"chars": 1062,
"preview": "// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the "
},
{
"path": "styles/bootstrap/mixins/labels.less",
"chars": 161,
"preview": "// Labels\n\n.label-variant(@color) {\n background-color: @color;\n\n &[href] {\n &:hover,\n &:focus {\n background"
},
{
"path": "styles/bootstrap/mixins/list-group.less",
"chars": 533,
"preview": "// List Groups\n\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;"
},
{
"path": "styles/bootstrap/mixins/nav-divider.less",
"chars": 232,
"preview": "// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n.nav-divider(@color: #e5e5e5) {\n"
},
{
"path": "styles/bootstrap/mixins/nav-vertical-align.less",
"chars": 364,
"preview": "// Navbar vertical align\n//\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so"
},
{
"path": "styles/bootstrap/mixins/opacity.less",
"chars": 148,
"preview": "// Opacity\n\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha"
},
{
"path": "styles/bootstrap/mixins/pagination.less",
"chars": 438,
"preview": "// Pagination\n\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n > li {\n > a,\n"
},
{
"path": "styles/bootstrap/mixins/panels.less",
"chars": 537,
"preview": "// Panels\n\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n"
},
{
"path": "styles/bootstrap/mixins/progress-bar.less",
"chars": 191,
"preview": "// Progress bars\n\n.progress-bar-variant(@color) {\n background-color: @color;\n\n // Deprecated parent class requirement "
},
{
"path": "styles/bootstrap/mixins/reset-filter.less",
"chars": 248,
"preview": "// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the I"
},
{
"path": "styles/bootstrap/mixins/resize.less",
"chars": 196,
"preview": "// Resize anything\n\n.resizable(@direction) {\n resize: @direction; // Options: horizontal, vertical, both\n overflow: au"
},
{
"path": "styles/bootstrap/mixins/responsive-visibility.less",
"chars": 343,
"preview": "// Responsive utilities\n\n//\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility("
},
{
"path": "styles/bootstrap/mixins/size.less",
"chars": 127,
"preview": "// Sizing shortcuts\n\n.size(@width; @height) {\n width: @width;\n height: @height;\n}\n\n.square(@size) {\n .size(@size; @si"
},
{
"path": "styles/bootstrap/mixins/tab-focus.less",
"chars": 159,
"preview": "// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus"
},
{
"path": "styles/bootstrap/mixins/table-row.less",
"chars": 700,
"preview": "// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` an"
},
{
"path": "styles/bootstrap/mixins/text-emphasis.less",
"chars": 116,
"preview": "// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n"
},
{
"path": "styles/bootstrap/mixins/text-overflow.less",
"chars": 162,
"preview": "// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-ove"
},
{
"path": "styles/bootstrap/mixins/vendor-prefixes.less",
"chars": 6606,
"preview": "// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our"
},
{
"path": "styles/bootstrap/mixins.less",
"chars": 1102,
"preview": "// Mixins\n// --------------------------------------------------\n\n// Utilities\n@import \"mixins/hide-text.less\";\n@import \""
},
{
"path": "styles/bootstrap/modals.less",
"chars": 3518,
"preview": "//\n// Modals\n// --------------------------------------------------\n\n// .modal-open - body class for killing the scr"
},
{
"path": "styles/bootstrap/navbar.less",
"chars": 14646,
"preview": "//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static n"
},
{
"path": "styles/bootstrap/navs.less",
"chars": 4979,
"preview": "//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// ------------------------------------"
},
{
"path": "styles/bootstrap/normalize.less",
"chars": 7650,
"preview": "/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prev"
},
{
"path": "styles/bootstrap/pager.less",
"chars": 861,
"preview": "//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n padding-left: 0;\n margin: @li"
},
{
"path": "styles/bootstrap/pagination.less",
"chars": 2001,
"preview": "//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n display: inline-"
},
{
"path": "styles/bootstrap/panels.less",
"chars": 6097,
"preview": "//\n// Panels\n// --------------------------------------------------\n\n\n// Base class\n.panel {\n margin-bottom: @line-heigh"
},
{
"path": "styles/bootstrap/popovers.less",
"chars": 3508,
"preview": "//\n// Popovers\n// --------------------------------------------------\n\n\n.popover {\n position: absolute;\n top: 0;\n left"
},
{
"path": "styles/bootstrap/print.less",
"chars": 2133,
"preview": "/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// =============================="
},
{
"path": "styles/bootstrap/progress-bars.less",
"chars": 1925,
"preview": "//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -----------------------"
},
{
"path": "styles/bootstrap/responsive-embed.less",
"chars": 564,
"preview": "// Embeds responsive\n//\n// Credit: Nicolas Gallagher and SUIT CSS.\n\n.embed-responsive {\n position: relative;\n display:"
},
{
"path": "styles/bootstrap/responsive-utilities.less",
"chars": 4262,
"preview": "//\n// Responsive: Utility classes\n// --------------------------------------------------\n\n\n// IE10 in Windows (Phone) 8\n/"
},
{
"path": "styles/bootstrap/scaffolding.less",
"chars": 2656,
"preview": "//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This r"
},
{
"path": "styles/bootstrap/tables.less",
"chars": 4610,
"preview": "//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n"
},
{
"path": "styles/bootstrap/theme.less",
"chars": 7694,
"preview": "\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@"
},
{
"path": "styles/bootstrap/thumbnails.less",
"chars": 753,
"preview": "//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.th"
},
{
"path": "styles/bootstrap/tooltip.less",
"chars": 2959,
"preview": "//\n// Tooltips\n// --------------------------------------------------\n\n\n// Base class\n.tooltip {\n position: absolute;\n "
},
{
"path": "styles/bootstrap/type.less",
"chars": 5959,
"preview": "//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h"
},
{
"path": "styles/bootstrap/utilities.less",
"chars": 780,
"preview": "//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.c"
},
{
"path": "styles/bootstrap/variables.less",
"chars": 26791,
"preview": "//\n// Variables\n// --------------------------------------------------\n\n\n//== Colors\n//\n//## Gray and brand colors for us"
},
{
"path": "styles/bootstrap/wells.less",
"chars": 527,
"preview": "//\n// Wells\n// --------------------------------------------------\n\n\n// Base class\n.well {\n min-height: 20px;\n padding:"
},
{
"path": "styles/container-home.less",
"chars": 4158,
"preview": ".details-panel.home {\n background-color: @color-background;\n overflow: hidden;\n .content {\n display: flex;\n fle"
},
{
"path": "styles/container-logs.less",
"chars": 723,
"preview": ".details-panel.logs {\n background-color: @gray-darkest;\n -webkit-user-select: text;\n font-family: @font-code;\n font-"
},
{
"path": "styles/container-progress.less",
"chars": 714,
"preview": ".container-progress {\n display: inline-block;\n position: relative;\n min-width: 100px;\n max-width: 100px;\n min-heigh"
},
{
"path": "styles/container-settings.less",
"chars": 4080,
"preview": ".details-panel .settings {\n display: flex;\n flex: 1 auto;\n flex-direction: column;\n background-color: white;\n margi"
},
{
"path": "styles/header.less",
"chars": 4018,
"preview": ".header {\n min-height: 40px;\n -webkit-app-region: drag;\n -webkit-user-select: none;\n\n &.bordered {\n border-bottom"
},
{
"path": "styles/icons.less",
"chars": 2028,
"preview": "@charset \"UTF-8\";\n\n@font-face {\n font-family: \"kitematic\";\n src:url(\"kitematic.eot\");\n src:url(\"kitematic.eot?#iefix\""
},
{
"path": "styles/layout.less",
"chars": 201,
"preview": ".containers {\n box-sizing: border-box;\n height: 100%;\n display: flex;\n flex-direction: column;\n .containers-body {\n"
},
{
"path": "styles/left-panel.less",
"chars": 8482,
"preview": "/* Sidebar */\n\n.sidebar {\n background-color: white;\n margin: 0;\n border-right: 1px solid @color-divider;\n display: f"
},
{
"path": "styles/loading.less",
"chars": 201,
"preview": ".loading {\n display: flex;\n width: 100%;\n height: 100%;\n flex-direction: column;\n\n .loading-content {\n display: "
},
{
"path": "styles/main.less",
"chars": 919,
"preview": "@import \"bootstrap/bootstrap.less\";\n@import \"variables.less\";\n@import \"mixins.less\";\n@import \"theme.less\";\n@import \"icon"
},
{
"path": "styles/mixins.less",
"chars": 1756,
"preview": ".traffic-light() {\n box-sizing: border-box;\n display: inline-block;\n background: white;\n margin-right: 8px;\n height"
},
{
"path": "styles/new-container.less",
"chars": 11833,
"preview": ".new-container-pull {\n display: flex;\n flex: 1 auto;\n align-items: center;\n justify-content: center;\n .content {\n "
},
{
"path": "styles/preferences.less",
"chars": 1256,
"preview": "@import \"variables.less\";\n\n.preferences {\n flex: 1 auto;\n display: flex;\n align-items: flex-start;\n justify-content:"
},
{
"path": "styles/radial.less",
"chars": 3287,
"preview": "@import \"variables.less\";\n\n@-webkit-keyframes rotating {\n from{\n -webkit-transform: rotate(0deg);\n }\n to{\n -web"
},
{
"path": "styles/retina.less",
"chars": 1628,
"preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2013 Imulus, LLC, Ben Atkin, and other contributors\n\nPermission is hereby grante"
},
{
"path": "styles/right-panel.less",
"chars": 4908,
"preview": ".details {\n background-color: @color-background;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n flex: 1 0;\n di"
},
{
"path": "styles/setup.less",
"chars": 4562,
"preview": ".setup {\n display: flex;\n height: 100%;\n width: 100%;\n flex-direction: column;\n //-webkit-app-region: drag;\n\n .set"
},
{
"path": "styles/spinner.less",
"chars": 2714,
"preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2015 Daniel Cardoso\n\nPermission is hereby granted, free of charge, to any p"
},
{
"path": "styles/theme.less",
"chars": 5403,
"preview": "//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"bootstrap/variables"
},
{
"path": "styles/variables.less",
"chars": 1235,
"preview": "@brand-primary: #22b8eb;\n@brand-action: @brand-primary;\n@brand-positive: #15CC35;\n@brand-negativ"
},
{
"path": "tsconfig.json",
"chars": 510,
"preview": "{\n \"$schema\": \"http://json.schemastore.org/tsconfig\",\n \"compilerOptions\": {\n \"checkJs\": false,\n \"charset\": \"utf-"
},
{
"path": "tslint.json",
"chars": 89,
"preview": "{\n \"$schema\": \"http://json.schemastore.org/tslint\",\n \"extends\": \"tslint:recommended\"\n}\n"
},
{
"path": "util/Info.plist",
"chars": 1139,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "util/VirtualBox_Uninstall.tool",
"chars": 11173,
"preview": "#!/bin/bash\n# $Id: VirtualBox_Uninstall.tool 89624 2013-10-07 16:13:23Z bird $\n## @file\n# VirtualBox Uninstaller Script."
},
{
"path": "util/prepare.js",
"chars": 41,
"preview": "require.requireActual('babel-polyfill');\n"
},
{
"path": "util/reset",
"chars": 277,
"preview": "#!/bin/bash\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nsudo rm -f /usr/local/bin/docker*\npkill VirtualBox"
},
{
"path": "util/reset.ps1",
"chars": 412,
"preview": "get-process VBox* | stop-process\n\n$paths = '~/Kitematic/', '~/.docker', '~/.VirtualBox/', '~/Kitematic-bins/', '~/Librar"
},
{
"path": "util/testenv.js",
"chars": 348,
"preview": "var mock = (function() {\n var store = {};\n return {\n getItem: function(key) {\n return store[key];\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the docker-archive-public/docker.kitematic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 194 files (561.1 KB), approximately 148.6k tokens, and a symbol index with 161 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.