Compare commits

...

59 Commits

Author SHA1 Message Date
Anthony Lapenna bcdd7498a1 chore(version): bump version number 2016-07-14 12:18:29 +12:00
Anthony Lapenna 4cc08d7211 refactor(ui): remove console logging 2016-07-14 12:07:07 +12:00
Anthony Lapenna 48b6b6340b Merge branch 'feat68-update-repository-field' into internal 2016-07-14 12:03:57 +12:00
Anthony Lapenna 1011fde9de feat(ui): replace repository field with tags field in image view 2016-07-14 12:01:57 +12:00
Anthony Lapenna bee89720d5 refactor(ui): fix lint issue 2016-07-14 11:31:58 +12:00
Anthony Lapenna 23bff41304 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-14 11:31:10 +12:00
Anthony Lapenna 8243326692 refactor(ui): fix lint issue 2016-07-14 11:29:41 +12:00
Anthony Lapenna 17ae122595 Merge branch 'style73-widget-header-extra-space' into internal 2016-07-14 11:25:01 +12:00
Anthony Lapenna b69d72fc8c Merge branch 'style72-table-style' into internal 2016-07-14 11:24:54 +12:00
Anthony Lapenna c52498993b Merge branch 'style70-typo-engine-view' into internal 2016-07-14 11:24:45 +12:00
Anthony Lapenna a4a82b4502 merge branch feat54-revamp-internal into internal 2016-07-14 11:24:20 +12:00
Anthony Lapenna 52d953a1c2 style(ui): fix typo in Engine view (#76) 2016-07-14 11:16:23 +12:00
Anthony Lapenna e145d82947 style(ui): fix extra space in widget-header (#78) 2016-07-14 11:16:16 +12:00
Anthony Lapenna 25df1fe26c style(ui): add table-hover class to all entity tables (#77) 2016-07-14 11:16:10 +12:00
Anthony Lapenna 106718f416 style(ui): fix extra space in widget-header 2016-07-14 11:10:38 +12:00
Anthony Lapenna 8464faa2a1 style(ui): add table-hover class to all entity tables 2016-07-14 11:03:40 +12:00
Anthony Lapenna 9354b911bb style(ui): fix typo in Engine view 2016-07-14 11:00:26 +12:00
Anthony Lapenna c8a5b82c89 feat(ui): new dashboard view (#75) 2016-07-14 10:58:39 +12:00
Anthony Lapenna 1f884e9584 chore(version): bumped version number 2016-07-13 15:06:10 +12:00
Anthony Lapenna 00b2c92e39 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-13 14:54:35 +12:00
Anthony Lapenna 0796778d17 Merge tag '1.4.0' into develop
Release 1.4.0
2016-07-13 14:54:07 +12:00
Anthony Lapenna 1eae1c03f0 Merge branch 'release/1.4.0' 2016-07-13 14:54:03 +12:00
Anthony Lapenna a9209da167 chore(version): bump version number 2016-07-13 14:53:24 +12:00
Anthony Lapenna 43c2f14289 docs(docker): add info about Docker version support (#64) 2016-07-13 14:47:24 +12:00
Anthony Lapenna f378d56543 fix(ui): fix bad name for image field in container creation view 2016-07-13 13:47:15 +12:00
Anthony Lapenna 521d146d7b refactor(ui): remove useless createNetwork sources 2016-07-13 13:46:06 +12:00
Anthony Lapenna dc721f5870 merge feat66-docker-cli-compliant 2016-07-13 13:43:58 +12:00
Anthony Lapenna 3b0d726c2a feat(dockerui): Docker CLI compliant flags (#67) 2016-07-13 12:44:31 +12:00
Anthony Lapenna f6226d19b8 feat(dockerui): Docker CLI compliant flags 2016-07-13 12:42:20 +12:00
Anthony Lapenna 71c091ae0d feat(ui): docker 1.9 support (#65) 2016-07-13 10:53:03 +12:00
Anthony Lapenna 1fb008212a feat(dockerui): add support for TLS enabled engines (#63) 2016-07-12 20:31:11 +12:00
Anthony Lapenna cab34e4069 feat(network): add advanced settings in network creation (subnet/gateway) 2016-07-08 17:26:18 +12:00
Anthony Lapenna e67e20ce18 feat(network): add the ability to specify a subnet/gateway when creating a network (#53) 2016-07-08 17:12:33 +12:00
Anthony Lapenna d253c0d494 chore(version): bump version number 2016-07-08 16:26:29 +12:00
Anthony Lapenna c74e8fc732 style(lint): fix jshint issue 2016-07-08 16:20:31 +12:00
Anthony Lapenna 29358e5744 Merge tag '1.3.0' into develop
Release 1.3.0
2016-07-08 16:06:58 +12:00
Anthony Lapenna b59c102098 Merge branch 'release/1.3.0' 2016-07-08 16:06:53 +12:00
Anthony Lapenna afaa1433ff chore(version): bump version number 2016-07-08 16:06:46 +12:00
Anthony Lapenna f923016052 style(lint): fix jshint issues 2016-07-08 15:50:16 +12:00
Anthony Lapenna ca27e7f27a fix(containerCreation): fix an issue when creating an image from a custom registry without automatic pulling (#50) 2016-07-08 15:40:13 +12:00
Anthony Lapenna 8fd9c2fce2 refactor(ui): remove useless logging statement 2016-07-08 15:39:32 +12:00
Anthony Lapenna d4ca060945 feat(ui): add the ability to pull an image from a selection of registry 2016-07-08 15:31:09 +12:00
Anthony Lapenna d124c21d1b feat(ui): add the ability to create a container from an image in a custom registry (#49) 2016-07-08 12:52:26 +12:00
Anthony Lapenna d2fb2cb863 feat(ui): add the ability to pull an image from a private registry (#47) 2016-07-08 11:57:24 +12:00
Anthony Lapenna 0350daca8d Merge branch 'hide-containers-dashboard-swarm' into internal 2016-07-07 15:38:42 +12:00
Anthony Lapenna 06f54e300c fix(ui): hidden containers (using label) are now removed from dashboard and swarm view (#46) 2016-07-07 15:37:09 +12:00
Anthony Lapenna 135b940897 refactor(grunt): remove testing option 2016-07-07 15:35:45 +12:00
Anthony Lapenna 7856276092 fix(ui): hidden containers (using label) are now removed from dashboard and swarm view 2016-07-07 15:34:05 +12:00
Anthony Lapenna bf14dcc3e8 merge display-all-containers 2016-07-07 14:39:26 +12:00
Anthony Lapenna 21c1778822 feat(ui): default to display all containers (#45) 2016-07-07 14:31:16 +12:00
Anthony Lapenna 337bfa74bb feat(ui): default to display all containers 2016-07-07 14:30:11 +12:00
Anthony Lapenna 418b1ff544 fix(ui): fix display issue with multiple nodes in Swarm view 2016-07-07 13:25:22 +12:00
Anthony Lapenna 092d866c73 fix(ui): fix display issue with multiple nodes in Swarm view (#44) 2016-07-07 13:22:31 +12:00
Anthony Lapenna 50391c87e2 feat(ui): replace ViewSpinner with JQuery animations (#43) 2016-07-07 13:17:44 +12:00
Anthony Lapenna fd6645d068 merge fix-viewspinner 2016-07-07 13:15:56 +12:00
Anthony Lapenna 3a6e326e5e feat(ui): replace ViewSpinner with JQuery animations 2016-07-07 12:44:58 +12:00
Anthony Lapenna b997b787c4 feat(ui): simplify views for internal usage 2016-07-06 19:04:45 +12:00
Anthony Lapenna d227bdfc75 refactor(ui): remove useless controller declarations 2016-07-06 17:42:56 +12:00
Anthony Lapenna 4ba6286c97 Merge tag '1.2.0' into develop
Release 1.2.0
2016-07-06 16:41:33 +12:00
36 changed files with 790 additions and 618 deletions
+55 -9
View File
@@ -1,5 +1,7 @@
# Cloudinovasi UI for Docker
This UI is dedicated to CloudInovasi internal usage.
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
![Dashboard](/dashboard.png)
@@ -10,6 +12,12 @@ UI For Docker is a web interface for the Docker Remote API. The goal is to prov
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
## Supported Docker versions
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
## Run
### Quickstart
@@ -26,11 +34,16 @@ The `--privileged` flag is required for hosts using SELinux.
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
You can use the `-e` flag to change this socket:
You can use the `--host`, `-H` flags to change this socket:
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
### Swarm support
@@ -41,7 +54,7 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://<SWARM_HOST>:<SWARM_PORT> --swarm
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
```
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
@@ -54,9 +67,27 @@ UI For Docker listens on port 9000 by default. If you run UI For Docker inside a
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
```
### Access a Docker engine protected via TLS
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
### Hide containers with specific labels
You can hide specific containers in the containers view by using the `-hide-label` or `-l` options and specifying a label.
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
For example, take a container started with the label `owner=acme`:
@@ -70,13 +101,28 @@ You can hide it in the view by starting the ui with:
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
```
### Custom Docker registries support
You can specify the support of others registries than DockerHub by using the `--registries` or `-r` options and specifying a registry using the format *REGISTRY_NAME=REGISTRY_ADDRESS*.
For example, if I want the registry 'myCustomRegistry' pointing to *myregistry.domain.com:5000* available in the UI:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -r myCustomRegistry=myregistry.domain.com:5000
```
### Available options
The following options are available for the `ui-for-docker` binary:
* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*)
* `--data`, `-d`: Path to the data folder (default: *"."*)
* `--assets`, `-a`: Path to the assets (default: *"."*)
* `--swarm`, `-s`: Swarm cluster support (default: *false*)
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--registries`, `-r`: Available registries in the UI (format *REGISTRY_NAME=REGISTRY_ADDRESS*)
* `--tlsverify`: TLS support (default: `false`)
* `--tlscacert`: Path to the CA (default `/certs/ca.pem`)
* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`)
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
+2 -4
View File
@@ -19,9 +19,7 @@ angular.module('uifordocker', [
'swarm',
'network',
'networks',
'createNetwork',
'volumes',
'createVolume'])
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
@@ -144,4 +142,4 @@ angular.module('uifordocker', [
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', '/config')
.constant('UI_VERSION', 'v1.2.0');
.constant('UI_VERSION', 'v1.5.0');
+3 -1
View File
@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Container details"></rd-header-title>
<rd-header-title title="Container details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>
+15 -15
View File
@@ -1,6 +1,6 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', 'ViewSpinner', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, ViewSpinner, $timeout) {
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
@@ -11,7 +11,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
var update = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$scope.container.edit = false;
@@ -61,7 +61,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
$scope.newCfg.Binds.push(bind);
});
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
@@ -69,13 +69,13 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
};
$scope.start = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
@@ -89,7 +89,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.stop = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.stop({id: $stateParams.id}, function (d) {
update();
Messages.send("Container stopped", $stateParams.id);
@@ -100,7 +100,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.kill = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.kill({id: $stateParams.id}, function (d) {
update();
Messages.send("Container killed", $stateParams.id);
@@ -111,7 +111,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.commit = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
update();
Messages.send("Container commited", $stateParams.id);
@@ -121,7 +121,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
});
};
$scope.pause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container paused", $stateParams.id);
@@ -132,7 +132,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.unpause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.unpause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container unpaused", $stateParams.id);
@@ -143,7 +143,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.remove = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
update();
$state.go('containers', {}, {reload: true});
@@ -155,7 +155,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.restart = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.restart({id: $stateParams.id}, function (d) {
update();
Messages.send("Container restarted", $stateParams.id);
@@ -170,10 +170,10 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.getChanges = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
};
@@ -1,6 +1,6 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
@@ -8,24 +8,24 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpi
$scope.stderr = '';
$scope.tailLines = 2000;
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
function getLogs() {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
getLogsStdout();
getLogsStderr();
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}
function getLogsStderr() {
@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Container logs"></rd-header-title>
<rd-header-title title="Container logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>
+8 -5
View File
@@ -10,6 +10,9 @@
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers">
<div class="pull-right">
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
@@ -25,7 +28,7 @@
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
</div>
<div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()"/><label for="displayAll">Display All</label>
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
@@ -36,7 +39,7 @@
<tr>
<th></th>
<th>
<a ui-sref="containers" ng-click="order('State')">
<a ui-sref="containers" ng-click="order('Status')">
State
<span ng-show="sortType == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
@@ -49,7 +52,7 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<th ng-if="state.displayIP">
<a ui-sref="containers" ng-click="order('IP')">
IP Address
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
@@ -82,10 +85,10 @@
<tbody>
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.State|containerstatusbadge }}">{{ container.State }}</span></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td>{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm">{{ container|swarmhostname}}</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:60 }}</td>
@@ -1,9 +1,10 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) {
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
@@ -14,7 +15,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
};
var update = function (data) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
@@ -22,19 +23,23 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
return new ContainerViewModel(container);
var model = new ContainerViewModel(container);
if (model.IP) {
$scope.state.displayIP = true;
}
return model;
});
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
});
};
var batch = function (items, action, msg) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
update({all: Settings.displayAll ? 1 : 0});
}
};
@@ -89,7 +94,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
}
});
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
}
};
@@ -1,6 +1,6 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, ViewSpinner, errorMsgFilter) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
$scope.state = {
alwaysPull: true
@@ -8,9 +8,13 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
$scope.formValues = {
Console: 'none',
Volumes: []
Volumes: [],
AvailableRegistries: [],
Registry: ''
};
$scope.imageConfig = {};
$scope.config = {
Env: [],
HostConfig: {
@@ -24,12 +28,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
}
};
$scope.resetVolumePath = function(index) {
$scope.formValues.Volumes[index].name = '';
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, isPath: false });
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
@@ -55,8 +55,15 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
Config.$promise.then(function (c) {
var swarm = c.swarm;
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
var persistedVolumes = d.Volumes.filter(function (volume) {
if (volume.Driver === 'local-persist') {
return volume;
}
});
$scope.availableVolumes = _.uniqBy(persistedVolumes, 'Name');
}, function (e) {
Messages.error("Failure", e.data);
});
@@ -69,6 +76,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Name: "bridge"});
networks.push({Name: "host"});
networks.push({Name: "none"});
@@ -80,59 +88,67 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
});
function createContainer(config) {
ViewSpinner.spin();
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
});
} else {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
});
}
function createImageConfig(imageName) {
var imageNameAndTag = imageName.split(':');
var imageConfig = {
fromImage: imageNameAndTag[0],
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
function pullImageAndCreateContainer(config) {
ViewSpinner.spin();
var image = _.toLower(config.Image);
var imageConfig = createImageConfig(image);
Image.create(imageConfig, function (data) {
$('#createContainerSpinner').show();
Image.create($scope.imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
}
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
function prepareImageConfig(config) {
var image = _.toLower(config.Image);
var registry = $scope.formValues.Registry;
var imageConfig = createImageConfig(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
function preparePortBindings(config) {
var bindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
@@ -192,6 +208,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareImageConfig(config);
preparePortBindings(config);
prepareConsole(config);
prepareEnvironmentVariables(config);
@@ -18,11 +18,18 @@
</div>
</div>
<!-- !name-input -->
<!-- image input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="ubuntu:trusty">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<select class="selectpicker form-control" ng-model="formValues.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
@@ -32,7 +39,7 @@
</div>
</div>
</div>
<!-- !image-input -->
<!-- !image-and-registry-inputs -->
<!-- restart-policy -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Restart policy</label>
@@ -208,28 +215,22 @@
<!-- volumes -->
<div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<div class="col-sm-11" ng-if="availableVolumes.length !== 0">
<span class="label label-default clickable" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span>
</div>
<div class="col-sm-11" ng-if="availableVolumes.length === 0" style="margin-top: 5px;">
<span class="small text-muted">You don't have any persistent volumes. Head over the <a ui-sref="volumes">volumes view</a> to create one.</span>
</div>
<!-- volumes-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
<div class="input-group col-sm-1 input-group-sm">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="volume.readOnly"> Read-only
</label>
</div>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
<select class="selectpicker form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:50}}</option>
</select>
<input ng-if="volume.isPath" type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
@@ -251,6 +252,11 @@
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0">
<div class="col-sm-12">
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
@@ -306,6 +312,9 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
</div>
@@ -1,57 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
$scope.formValues = {
DriverOptions: []
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createNetwork(config) {
ViewSpinner.spin();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
ViewSpinner.stop();
$state.go('networks', {}, {reload: true});
} else {
ViewSpinner.stop();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
Messages.error('Unable to create network', e.data);
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.Options = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -1,80 +0,0 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
Networks > Add network
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- driver-input -->
<div class="form-group">
<label for="network_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="option.name" placeholder="e.g. com.docker.network.bridge.enable_icc">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. true">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<!-- internal -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.Internal"> Restrict external access to the network
</label>
</div>
</div>
</div>
<!-- !internal -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>
</div>
@@ -1,56 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, ViewSpinner, errorMsgFilter) {
$scope.formValues = {
DriverOptions: []
};
$scope.config = {
Driver: 'local'
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createVolume(config) {
ViewSpinner.spin();
Volume.create(config, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
ViewSpinner.stop();
$state.go('volumes', {}, {reload: true});
} else {
ViewSpinner.stop();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
Messages.error('Unable to create volume', e.data);
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.DriverOpts = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createVolume(config);
};
}]);
@@ -1,69 +0,0 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-content>
Volumes > Add volume
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. myVolume">
</div>
</div>
<!-- !name-input -->
<!-- driver-input -->
<div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="option.name" placeholder="e.g. mountpoint">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. /path/on/host">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
</div>
</div>
+112 -57
View File
@@ -1,76 +1,131 @@
<rd-header>
<rd-header-title title="Home"></rd-header-title>
<rd-header-title title="Home">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
<rd-widget-header icon="fa-cogs" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>{{ infoData.Name }}</td>
</tr>
<tr>
<td>Docker version</td>
<td>{{ infoData.ServerVersion }}</td>
</tr>
<tr>
<td>CPU</td>
<td>{{ infoData.NCPU }}</td>
</tr>
<tr>
<td>Memory</td>
<td>{{ infoData.MemTotal|humansize }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
<rd-widget>
<rd-widget-body>
<div class="widget-icon green pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.running }}</div>
<div class="comment">Running</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon red pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.stopped }}</div>
<div class="comment">Stopped</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon gray pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.ghost }}</div>
<div class="comment">Ghost</div>
<rd-widget-header icon="fa-cogs" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td>{{ infoData.SystemStatus[3][1] }}</td>
</tr>
<tr>
<td>Swarm version</td>
<td>{{ infoData.ServerVersion|swarmversion }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td>{{ infoData.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td>{{ infoData.MemTotal|humansize }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers created"></rd-widget-header>
<rd-widget-body>
<canvas id="containers-started-chart" width="770" height="230">
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
</canvas>
</rd-widget-body>
</rd-widget>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="containers">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-heartbeat text-icon green-icon"></i>{{ containerData.running }} running</div>
<div><i class="fa fa-heartbeat text-icon red-icon"></i>{{ containerData.stopped }} stopped</div>
</div>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images created"></rd-widget-header>
<rd-widget-body>
<canvas id="images-created-chart" width="770" height="230">
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
</canvas>
</rd-widget-body>
</rd-widget>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="images">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-clone"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div>
</div>
<div class="title">{{ imageData.total }}</div>
<div class="comment">Images</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="volumes">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-cubes"></i>
</div>
<div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div>
</div>
<div class="title">{{ volumeData.total }}</div>
<div class="comment">Volumes</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="networks">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-sitemap"></i>
</div>
<div class="title">{{ networkData.total }}</div>
<div class="comment">Networks</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
</div>
<div class="row">
</div>
+85 -27
View File
@@ -1,46 +1,104 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.containerData = {};
var buildCharts = function (data) {
$scope.containerData.total = data.length;
LineChart.build('#containers-started-chart', data, function (c) {
return new Date(c.Created * 1000).toLocaleDateString();
});
var s = $scope;
Image.query({}, function (d) {
s.totalImages = d.length;
LineChart.build('#images-created-chart', d, function (c) {
return new Date(c.Created * 1000).toLocaleDateString();
});
});
$scope.containerData = {
total: 0
};
$scope.imageData = {
total: 0
};
$scope.networkData = {
total: 0
};
$scope.volumeData = {
total: 0
};
Container.query({all: 1}, function (d) {
function prepareContainerData(d) {
var running = 0;
var ghost = 0;
var stopped = 0;
// TODO: centralize that
var containers = d.filter(function (container) {
return container.Image !== 'swarm';
});
var containers = d;
if (hiddenLabels) {
containers = hideContainers(d);
}
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
if (item.Status === "Ghost") {
ghost += 1;
if (item.Status.indexOf('Up') !== -1) {
running += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.ghost = ghost;
$scope.containerData.total = containers.length;
}
buildCharts(containers);
function prepareImageData(d) {
var images = d;
var totalImageSize = 0;
for (var i = 0; i < images.length; i++) {
var item = images[i];
totalImageSize += item.VirtualSize;
}
$scope.imageData.total = images.length;
$scope.imageData.size = totalImageSize;
}
function prepareVolumeData(d) {
var volumes = d.Volumes;
$scope.volumeData.total = volumes.length;
}
function prepareNetworkData(d) {
var networks = d;
$scope.networkData.total = networks.length;
}
function prepareInfoData(d) {
var info = d;
$scope.infoData = info;
}
function fetchDashboardData() {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
Image.query({}).$promise,
Volume.query({}).$promise,
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0]);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
prepareInfoData(d[4]);
$('#loadingViewSpinner').hide();
});
}
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});
}]);
+1 -1
View File
@@ -45,7 +45,7 @@
<div class="row">
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
+20 -16
View File
@@ -4,7 +4,6 @@
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Images</rd-header-content>
</rd-header>
@@ -15,14 +14,21 @@
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in availableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-input -->
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -33,6 +39,7 @@
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
@@ -45,6 +52,9 @@
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images">
<div class="pull-right">
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
@@ -56,7 +66,7 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></th>
@@ -69,34 +79,28 @@
</th>
<th>
<a ui-sref="images" ng-click="order('RepoTags')">
Repository
Tags
<span ng-show="sortType == 'RepoTags' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'RepoTags' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('VirtualSize')">
VirtualSize
Size
<span ng-show="sortType == 'VirtualSize' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>{{ image|repotag }}</td>
<td>
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getdate }}</td>
</tr>
</tbody>
</table>
+27 -17
View File
@@ -1,14 +1,15 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Image', 'ViewSpinner', 'Messages',
function ($scope, $state, Image, ViewSpinner, Messages) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
function ($scope, $state, Config, Image, Messages) {
$scope.state = {};
$scope.sortType = 'Created';
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.config = {
Image: ''
Image: '',
Registry: ''
};
$scope.order = function(sortType) {
@@ -35,42 +36,47 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
}
};
function createImageConfig(imageName) {
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: imageNameAndTag[0],
fromImage: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.pullImage = function() {
ViewSpinner.spin();
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var imageConfig = createImageConfig(image);
var registry = $scope.config.Registry;
var imageConfig = createImageConfig(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
ViewSpinner.stop();
$('#pullImageSpinner').hide();
Messages.error('Error', detail.error);
} else {
ViewSpinner.stop();
$('#pullImageSpinner').hide();
$state.go('images', {}, {reload: true});
}
}, function (e) {
ViewSpinner.stop();
$('#pullImageSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}
};
angular.forEach($scope.images, function (i) {
@@ -85,6 +91,7 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
complete();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
complete();
});
}
@@ -92,17 +99,20 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
};
function fetchImages() {
ViewSpinner.spin();
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
});
}
fetchImages();
Config.$promise.then(function (c) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);
+3 -1
View File
@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Network details"></rd-header-title>
<rd-header-title title="Network details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content>
+11 -11
View File
@@ -1,37 +1,37 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, $state, $stateParams, errorMsgFilter) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.disconnect = function disconnect(networkId, containerId) {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
});
};
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
}]);
+66 -33
View File
@@ -1,5 +1,3 @@
<div ng-include="template" ng-controller="CreateNetworkController"></div>
<rd-header>
<rd-header-title title="Network list">
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
@@ -10,14 +8,73 @@
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a shared network">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- advanced-settings-input -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="state.advancedSettings"> Show advanced settings
</label>
</div>
</div>
</div>
<!-- !advanced-settings-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group" ng-if="state.advancedSettings">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
<div class="pull-right">
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.network">Add network</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -25,7 +82,7 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
@@ -36,13 +93,6 @@
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Id')">
Id
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Scope')">
Scope
@@ -50,30 +100,16 @@
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Driver')">
Driver
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
IPAM Driver
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
IPAM Subnet
Subnet
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
IPAM Gateway
Gateway
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
@@ -83,13 +119,10 @@
<tbody>
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:20}}</a></td>
<td>{{ network.Id }}</td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
</tr>
</tbody>
</table>
+58 -10
View File
@@ -1,12 +1,24 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', 'Network', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.state.advancedSettings = false;
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.formValues = {
Subnet: '',
Gateway: ''
};
$scope.config = {
Name: '',
IPAM: {
Config: []
}
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -32,13 +44,49 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
}
};
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
config.Driver = 'overlay';
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
} else {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
});
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}
};
angular.forEach($scope.networks, function (network) {
@@ -58,13 +106,13 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
};
function fetchNetworks() {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d;
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
});
}
fetchNetworks();
-12
View File
@@ -54,10 +54,6 @@
<td>Nodes</td>
<td>{{ swarm.Nodes }}</td>
</tr>
<tr>
<td>Containers</td>
<td>{{ info.Containers }}</td>
</tr>
<tr>
<td>Images</td>
<td>{{ info.Images }}</td>
@@ -108,13 +104,6 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Containers')">
Containers
<span ng-show="sortType == 'Containers' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Containers' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Engine')">
Engine
@@ -135,7 +124,6 @@
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
<td>{{ node.name }}</td>
<td>{{ node.ip }}</td>
<td>{{ node.containers }}</td>
<td>{{ node.version }}</td>
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
</tr>
+1 -1
View File
@@ -42,7 +42,7 @@ angular.module('swarm', [])
var node_offset = 4;
for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset);
node_offset += 10;
node_offset += 9;
}
}
+39 -13
View File
@@ -1,5 +1,3 @@
<div ng-include="template" ng-controller="CreateVolumeController"></div>
<rd-header>
<rd-header-title title="Volume list">
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
@@ -9,14 +7,50 @@
<rd-header-content>Volumes</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a persistent volume">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. mysql-data">
</div>
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: The volume will be created in our persisted storage and will be available across all the hosts of your cluster.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createVolume()">Create</button>
<i id="createVolumeSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
<div class="pull-right">
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -24,7 +58,7 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
@@ -42,21 +76,13 @@
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="volumes" ng-click="order('Mountpoint')">
Mountpoint
<span ng-show="sortType == 'Mountpoint' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:20 }}</td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
</tbody>
</table>
+39 -9
View File
@@ -1,12 +1,16 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', 'Volume', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortType = 'Driver';
$scope.sortReverse = true;
$scope.config = {
Name: ''
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
@@ -31,13 +35,39 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
}
};
function prepareVolumeConfiguration() {
var config = angular.copy($scope.config);
config.Driver = 'local-persist';
config.DriverOpts = {};
config.DriverOpts.mountpoint = '/volume/' + config.Name;
return config;
}
$scope.createVolume = function() {
$('#createVolumeSpinner').show();
var config = prepareVolumeConfiguration();
Volume.create(config, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
} else {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', e.data);
});
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadVolumesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadVolumesSpinner').hide();
}
};
angular.forEach($scope.volumes, function (volume) {
@@ -57,13 +87,13 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
};
function fetchVolumes() {
ViewSpinner.spin();
$('#loadVolumesSpinner').show();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes;
ViewSpinner.stop();
$scope.volumes = _.uniqBy(d.Volumes, 'Name');
$('#loadVolumesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadVolumesSpinner').hide();
});
}
fetchVolumes();
+1 -1
View File
@@ -8,7 +8,7 @@ angular
icon: '@'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </div><div class="pull-right col-xs-6 col-sm-4" ng-transclude></div></div></div>',
template: '<div class="widget-header"><div class="row"><span class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
restrict: 'E'
};
return directive;
+28 -7
View File
@@ -21,16 +21,31 @@ angular.module('dockerui.filters', [])
.filter('containerstatusbadge', function () {
'use strict';
return function (text) {
if (text === 'paused') {
var status = _.toLower(text);
if (status.indexOf('paused') !== -1) {
return 'warning';
} else if (text === 'created') {
} else if (status.indexOf('created') !== -1) {
return 'info';
} else if (text === 'exited') {
} else if (status.indexOf('exited') !== -1) {
return 'danger';
}
return 'success';
};
})
.filter('containerstatus', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
if (status.indexOf('paused') !== -1) {
return 'paused';
} else if (status.indexOf('created') !== -1) {
return 'created';
} else if (status.indexOf('exited') !== -1) {
return 'stopped';
}
return 'running';
};
})
.filter('nodestatusbadge', function () {
'use strict';
return function (text) {
@@ -115,23 +130,29 @@ angular.module('dockerui.filters', [])
return _.split(container.Names[0], '/')[2];
};
})
.filter('swarmversion', function () {
'use strict';
return function (text) {
return _.split(text, '/')[1];
};
})
.filter('swarmhostname', function () {
'use strict';
return function (container) {
return _.split(container.Names[0], '/')[1];
};
})
.filter('repotag', function () {
.filter('repotags', function () {
'use strict';
return function (image) {
if (image.RepoTags && image.RepoTags.length > 0) {
var tag = image.RepoTags[0];
if (tag === '<none>:<none>') {
tag = '';
return [];
}
return tag;
return image.RepoTags;
}
return '';
return [];
};
})
.filter('getdate', function () {
+1 -15
View File
@@ -153,27 +153,13 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
}
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
return {
displayAll: false,
displayAll: true,
endpoint: DOCKER_ENDPOINT,
uiVersion: UI_VERSION,
url: url,
firstLoad: firstLoad
};
}])
.factory('ViewSpinner', function ViewSpinnerFactory() {
'use strict';
var spinner = new Spinner();
var target = document.getElementById('view');
return {
spin: function () {
spinner.spin(target);
},
stop: function () {
spinner.stop();
}
};
})
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
'use strict';
return {
+5 -2
View File
@@ -10,9 +10,12 @@ function ImageViewModel(data) {
function ContainerViewModel(data) {
this.Id = data.Id;
this.State = data.State;
this.Status = data.Status;
this.Names = data.Names;
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
// Unavailable in Docker < 1.10
if (data.NetworkSettings) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
+20
View File
@@ -165,3 +165,23 @@ input[type="radio"] {
.clickable {
cursor: pointer;
}
.text-icon {
margin-right: 5px;
}
.green-icon {
color: #23ae89;
}
.red-icon {
color: #ae2323;
}
.image-tag {
margin-right: 5px;
}
.widget .widget-body table tbody .image-tag {
font-size: 90% !important;
}
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "uifordocker",
"version": "1.2.0",
"version": "1.5.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Michael Crosby <crosbymichael@gmail.com>",
@@ -34,12 +34,11 @@
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"bootstrap": "~3.3.6",
"font-awesome": "~4.5.0",
"font-awesome": "~4.6.3",
"jquery": "1.11.1",
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"spin.js": "1.3"
},
"resolutions": {
"angular": "1.5.5"
+78 -20
View File
@@ -15,15 +15,22 @@ import (
"fmt"
"github.com/gorilla/securecookie"
"gopkg.in/alecthomas/kingpin.v2"
"crypto/tls"
"crypto/x509"
)
var (
endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').String()
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
registries = LabelParser(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
authKey []byte
authKeyFile = "authKey.dat"
)
@@ -32,9 +39,17 @@ type UnixHandler struct {
path string
}
type TlsFlags struct {
tls bool
caPath string
certPath string
keyPath string
}
type Config struct {
Swarm bool `json:"swarm"`
HiddenLabels Labels `json:"hiddenLabels"`
Registries Labels `json:"registries"`
}
type Label struct {
@@ -47,7 +62,7 @@ type Labels []Label
func (l *Labels) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected HEADER=VALUE got '%s'", value)
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
}
label := new(Label)
label.Name = parts[0]
@@ -106,35 +121,70 @@ func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
json.NewEncoder(w).Encode(c)
}
func createTcpHandler(e string) http.Handler {
u, err := url.Parse(e)
func createTcpHandler(u *url.URL) http.Handler {
u.Scheme = "http";
return httputil.NewSingleHostReverseProxy(u)
}
func createTlsConfig(tlsFlags TlsFlags) *tls.Config {
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
if err != nil {
log.Fatal(err)
}
return httputil.NewSingleHostReverseProxy(u)
caCert, err := ioutil.ReadFile(tlsFlags.caPath)
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
return tlsConfig;
}
func createTcpHandlerWithTLS(u *url.URL, tlsFlags TlsFlags) http.Handler {
u.Scheme = "https";
var tlsConfig = createTlsConfig(tlsFlags)
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
return proxy;
}
func createUnixHandler(e string) http.Handler {
return &UnixHandler{e}
}
func createHandler(dir string, d string, e string, c Config) http.Handler {
func createHandler(dir string, d string, e string, c Config, tlsFlags TlsFlags) http.Handler {
var (
mux = http.NewServeMux()
fileHandler = http.FileServer(http.Dir(dir))
h http.Handler
)
if strings.Contains(e, "http") {
h = createTcpHandler(e)
} else {
if _, err := os.Stat(e); err != nil {
u, perr := url.Parse(e)
if perr != nil {
log.Fatal(perr)
}
if u.Scheme == "tcp" {
if tlsFlags.tls {
h = createTcpHandlerWithTLS(u, tlsFlags)
} else {
h = createTcpHandler(u)
}
} else if u.Scheme == "unix" {
var socketPath = u.Path
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
log.Fatalf("unix socket %s does not exist", e)
log.Fatalf("unix socket %s does not exist", socketPath)
}
log.Fatal(err)
}
h = createUnixHandler(e)
h = createUnixHandler(socketPath)
} else {
log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e)
}
// Use existing csrf authKey if present or generate a new one.
@@ -173,15 +223,23 @@ func csrfWrapper(h http.Handler) http.Handler {
}
func main() {
kingpin.Version("1.2.0")
kingpin.Version("1.5.0")
kingpin.Parse()
configuration := Config{
Swarm: *swarm,
HiddenLabels: *labels,
Registries: *registries,
}
handler := createHandler(*assets, *data, *endpoint, configuration)
tlsFlags := TlsFlags{
tls: *tlsverify,
caPath: *tlscacert,
certPath: *tlscert,
keyPath: *tlskey,
}
handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags)
if err := http.ListenAndServe(*addr, handler); err != nil {
log.Fatal(err)
}
+13 -2
View File
@@ -40,6 +40,7 @@ module.exports = function (grunt) {
grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
grunt.registerTask('run-swarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
grunt.registerTask('run-ssl', ['if:binaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
grunt.registerTask('clear', ['clean:app']);
// Print a timestamp (useful for when watching)
@@ -69,7 +70,6 @@ module.exports = function (grunt) {
'bower_components/jquery/dist/jquery.min.js',
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
'bower_components/bootstrap/dist/js/bootstrap.min.js',
'bower_components/spin.js/spin.js',
'bower_components/Chart.js/Chart.min.js',
'bower_components/lodash/dist/lodash.min.js',
'bower_components/oboe/dist/oboe-browser.js',
@@ -225,6 +225,10 @@ module.exports = function (grunt) {
buildSwarm: {
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
},
buildSsl: {
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
}
},
jshint: {
@@ -268,7 +272,14 @@ module.exports = function (grunt) {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 --swarm -d /data'
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:4000 --swarm -d /data'
].join(';')
},
runSsl: {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify'
].join(';')
},
cleanImages: {
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.2.0",
"version": "1.5.0",
"repository": {
"type": "git",
"url": "git@github.com:kevana/ui-for-docker.git"