Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony Lapenna b997b787c4 feat(ui): simplify views for internal usage 2016-07-06 19:04:45 +12:00
32 changed files with 360 additions and 742 deletions
+8 -41
View File
@@ -10,12 +10,6 @@ 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
@@ -32,16 +26,11 @@ 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 `--host`, `-H` flags to change this socket:
You can use the `-e` flag to change this socket:
```
# Connect to a tcp socket:
$ 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
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
```
### Swarm support
@@ -52,7 +41,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 -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://<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.
@@ -65,24 +54,6 @@ 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.
@@ -103,13 +74,9 @@ $ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docke
The following options are available for the `ui-for-docker` binary:
* `--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`)
* `--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*)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
* `--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`)
+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.4.0');
.constant('UI_VERSION', 'v1.2.0');
+1 -3
View File
@@ -1,7 +1,5 @@
<rd-header>
<rd-header-title title="Container details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-title title="Container details"></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', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', 'ViewSpinner', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, ViewSpinner, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
@@ -11,7 +11,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
var update = function () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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);
});
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
}, 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);
}
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
});
};
$scope.start = function () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
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 () {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
});
};
@@ -1,6 +1,6 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
@@ -8,24 +8,24 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.stderr = '';
$scope.tailLines = 2000;
$('#loadingViewSpinner').show();
ViewSpinner.spin();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
});
function getLogs() {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
getLogsStdout();
getLogsStderr();
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
}
function getLogsStderr() {
@@ -1,7 +1,5 @@
<rd-header>
<rd-header-title title="Container logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-title title="Container logs"></rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>
+5 -8
View File
@@ -10,9 +10,6 @@
<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">
@@ -28,7 +25,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()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()"/><label for="displayAll">Display All</label>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
@@ -39,7 +36,7 @@
<tr>
<th></th>
<th>
<a ui-sref="containers" ng-click="order('Status')">
<a ui-sref="containers" ng-click="order('State')">
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>
@@ -52,7 +49,7 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="state.displayIP">
<th>
<a ui-sref="containers" ng-click="order('IP')">
IP Address
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
@@ -85,10 +82,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.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td><span class="label label-{{ container.State|containerstatusbadge }}">{{ container.State }}</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 ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td>{{ 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,10 +1,9 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
@@ -15,7 +14,7 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
};
var update = function (data) {
$('#loadContainersSpinner').show();
ViewSpinner.spin();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
@@ -23,23 +22,19 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
if (model.IP) {
$scope.state.displayIP = true;
}
return model;
return new ContainerViewModel(container);
});
$('#loadContainersSpinner').hide();
ViewSpinner.stop();
});
};
var batch = function (items, action, msg) {
$('#loadContainersSpinner').show();
ViewSpinner.spin();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadContainersSpinner').hide();
ViewSpinner.stop();
update({all: Settings.displayAll ? 1 : 0});
}
};
@@ -94,7 +89,7 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
}
});
if (counter === 0) {
$('#loadContainersSpinner').hide();
ViewSpinner.stop();
}
};
@@ -1,6 +1,6 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, ViewSpinner, errorMsgFilter) {
$scope.state = {
alwaysPull: true
@@ -8,12 +8,9 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
$scope.formValues = {
Console: 'none',
Volumes: [],
Registry: ''
Volumes: []
};
$scope.imageConfig = {};
$scope.config = {
Env: [],
HostConfig: {
@@ -27,12 +24,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
}
};
$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) {
@@ -59,7 +52,12 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
var swarm = c.swarm;
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);
});
@@ -72,6 +70,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Name: "bridge"});
networks.push({Name: "host"});
networks.push({Name: "none"});
@@ -83,65 +82,57 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
});
function createContainer(config) {
$('#createContainerSpinner').show();
ViewSpinner.spin();
Container.create(config, function (d) {
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
$('#createContainerSpinner').hide();
ViewSpinner.stop();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
$('#createContainerSpinner').hide();
ViewSpinner.stop();
Messages.error('Error', errorMsgFilter(e));
});
} else {
$('#createContainerSpinner').hide();
ViewSpinner.stop();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#createContainerSpinner').hide();
ViewSpinner.stop();
Messages.error('Error', errorMsgFilter(e));
});
}
function pullImageAndCreateContainer(config) {
$('#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];
$('#createContainerSpinner').hide();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
}
function createImageConfig(imageName, registry) {
function createImageConfig(imageName) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: image,
fromImage: imageNameAndTag[0],
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
function prepareImageConfig(config) {
function pullImageAndCreateContainer(config) {
ViewSpinner.spin();
var image = _.toLower(config.Image);
var registry = $scope.formValues.Registry;
var imageConfig = createImageConfig(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
var imageConfig = createImageConfig(image);
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();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
ViewSpinner.stop();
Messages.error('Error', 'Unable to pull image ' + image);
});
}
function preparePortBindings(config) {
@@ -203,7 +194,6 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareImageConfig(config);
preparePortBindings(config);
prepareConsole(config);
prepareEnvironmentVariables(config);
@@ -18,15 +18,11 @@
</div>
</div>
<!-- !name-input -->
<!-- image-and-registry-inputs -->
<!-- image input -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<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">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="ubuntu:trusty">
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
@@ -36,7 +32,7 @@
</div>
</div>
</div>
<!-- !image-and-registry-inputs -->
<!-- !image-input -->
<!-- restart-policy -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Restart policy</label>
@@ -212,28 +208,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>
@@ -255,6 +245,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>
@@ -310,9 +305,6 @@
<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,74 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter',
function ($scope, $state, Messages, Network, errorMsgFilter) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: ''
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false,
IPAM: {
Config: []
}
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createNetwork(config) {
$('#createNetworkSpinner').show();
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);
});
}
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 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);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -1,95 +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 -->
<!-- subnet-gateway-inputs -->
<div class="form-group">
<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 -->
<!-- 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;">
<div>
<i id="createNetworkSpinner" 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="networks">Cancel</a>
</div>
</div>
@@ -1,56 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, 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) {
$('#createVolumeSpinner').show();
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);
});
}
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,72 +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;">
<div>
<i id="createVolumeSpinner" 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="volumes">Cancel</a>
</div>
</div>
+21 -44
View File
@@ -1,6 +1,5 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Config', 'Container', 'Image', 'Settings', 'LineChart',
function ($scope, Config, Container, Image, Settings, LineChart) {
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
$scope.containerData = {};
@@ -18,52 +17,30 @@ function ($scope, Config, Container, Image, Settings, LineChart) {
});
};
function fetchDashboardData() {
Container.query({all: 1}, function (d) {
var running = 0;
var ghost = 0;
var stopped = 0;
Container.query({all: 1}, function (d) {
var running = 0;
var ghost = 0;
var stopped = 0;
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;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.ghost = ghost;
buildCharts(containers);
// TODO: centralize that
var containers = d.filter(function (container) {
return container.Image !== 'swarm';
});
}
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;
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
if (item.Status === "Ghost") {
ghost += 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;
Config.$promise.then(function (c) {
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
buildCharts(containers);
});
}]);
+5 -20
View File
@@ -4,6 +4,7 @@
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Images</rd-header-content>
</rd-header>
@@ -14,18 +15,14 @@
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-and-registry-inputs -->
<!-- name-input -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<div class="col-sm-11">
<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">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -36,7 +33,6 @@
<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>
@@ -49,9 +45,6 @@
<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">
@@ -83,18 +76,11 @@
</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>
@@ -103,7 +89,6 @@
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>{{ image|repotag }}</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getdate }}</td>
</tr>
</tbody>
</table>
+16 -22
View File
@@ -1,15 +1,14 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Image', 'Messages',
function ($scope, $state, Image, Messages) {
.controller('ImagesController', ['$scope', '$state', 'Image', 'ViewSpinner', 'Messages',
function ($scope, $state, Image, ViewSpinner, Messages) {
$scope.state = {};
$scope.sortType = 'Created';
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.config = {
Image: '',
Registry: ''
Image: ''
};
$scope.order = function(sortType) {
@@ -36,47 +35,42 @@ function ($scope, $state, Image, Messages) {
}
};
function createImageConfig(imageName, registry) {
function createImageConfig(imageName) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: image,
fromImage: imageNameAndTag[0],
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.pullImage = function() {
$('#pullImageSpinner').show();
ViewSpinner.spin();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = createImageConfig(image, registry);
var imageConfig = createImageConfig(image);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
ViewSpinner.stop();
Messages.error('Error', detail.error);
} else {
$('#pullImageSpinner').hide();
ViewSpinner.stop();
$state.go('images', {}, {reload: true});
}
}, function (e) {
$('#pullImageSpinner').hide();
ViewSpinner.stop();
Messages.error('Error', 'Unable to pull image ' + image);
});
};
$scope.removeAction = function () {
$('#loadImagesSpinner').show();
ViewSpinner.spin();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadImagesSpinner').hide();
ViewSpinner.stop();
}
};
angular.forEach($scope.images, function (i) {
@@ -91,7 +85,6 @@ function ($scope, $state, Image, Messages) {
complete();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
complete();
});
}
@@ -99,14 +92,15 @@ function ($scope, $state, Image, Messages) {
};
function fetchImages() {
ViewSpinner.spin();
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
$('#loadImagesSpinner').hide();
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
ViewSpinner.stop();
});
}
+1 -3
View File
@@ -1,7 +1,5 @@
<rd-header>
<rd-header-title title="Network details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-title title="Network details"></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', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
.controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, $state, $stateParams, errorMsgFilter) {
$scope.disconnect = function disconnect(networkId, containerId) {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show();
ViewSpinner.spin();
Network.remove({id: $stateParams.id}, function (d) {
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
Messages.error("Failure", e.data);
});
};
$('#loadingViewSpinner').show();
ViewSpinner.spin();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
ViewSpinner.stop();
});
}]);
+38 -33
View File
@@ -7,17 +7,46 @@
<rd-header-content>Networks</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 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 -->
<!-- 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>
</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" />
@@ -36,13 +65,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 +72,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 +91,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>
+37 -10
View File
@@ -1,12 +1,15 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, Network, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, ViewSpinner, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.config = {
Name: ''
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -32,13 +35,37 @@ function ($scope, Network, Messages, errorMsgFilter) {
}
};
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
config.Driver = 'overlay';
return config;
}
$scope.createNetwork = function() {
ViewSpinner.spin();
var config = prepareNetworkConfiguration();
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);
});
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
ViewSpinner.spin();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadNetworksSpinner').hide();
ViewSpinner.stop();
}
};
angular.forEach($scope.networks, function (network) {
@@ -58,13 +85,13 @@ function ($scope, Network, Messages, errorMsgFilter) {
};
function fetchNetworks() {
$('#loadNetworksSpinner').show();
ViewSpinner.spin();
Network.query({}, function (d) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadNetworksSpinner').hide();
ViewSpinner.stop();
});
}
fetchNetworks();
+12
View File
@@ -54,6 +54,10 @@
<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>
@@ -104,6 +108,13 @@
<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
@@ -124,6 +135,7 @@
<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 += 9;
node_offset += 10;
}
}
+34 -13
View File
@@ -7,17 +7,46 @@
<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>
</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" />
@@ -43,21 +72,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', 'Messages', 'errorMsgFilter',
function ($scope, Volume, Messages, errorMsgFilter) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, ViewSpinner, 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, 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() {
ViewSpinner.spin();
var config = prepareVolumeConfiguration();
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);
});
};
$scope.removeAction = function () {
$('#loadVolumesSpinner').show();
ViewSpinner.spin();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadVolumesSpinner').hide();
ViewSpinner.stop();
}
};
angular.forEach($scope.volumes, function (volume) {
@@ -57,13 +87,13 @@ function ($scope, Volume, Messages, errorMsgFilter) {
};
function fetchVolumes() {
$('#loadVolumesSpinner').show();
ViewSpinner.spin();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes;
$('#loadVolumesSpinner').hide();
$scope.volumes = _.uniqBy(d.Volumes, 'Name');
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadVolumesSpinner').hide();
ViewSpinner.stop();
});
}
fetchVolumes();
+3 -18
View File
@@ -21,31 +21,16 @@ angular.module('dockerui.filters', [])
.filter('containerstatusbadge', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
if (status.indexOf('paused') !== -1) {
if (text === 'paused') {
return 'warning';
} else if (status.indexOf('created') !== -1) {
} else if (text === 'created') {
return 'info';
} else if (status.indexOf('exited') !== -1) {
} else if (text === 'exited') {
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) {
+15 -1
View File
@@ -153,13 +153,27 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
}
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
return {
displayAll: true,
displayAll: false,
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 {
+2 -5
View File
@@ -10,12 +10,9 @@ function ImageViewModel(data) {
function ContainerViewModel(data) {
this.Id = data.Id;
this.Status = data.Status;
this.State = data.State;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "uifordocker",
"version": "1.4.0",
"version": "1.2.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Michael Crosby <crosbymichael@gmail.com>",
@@ -34,11 +34,12 @@
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"font-awesome": "~4.5.0",
"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"
+21 -76
View File
@@ -15,21 +15,15 @@ import (
"fmt"
"github.com/gorilla/securecookie"
"gopkg.in/alecthomas/kingpin.v2"
"crypto/tls"
"crypto/x509"
)
var (
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()
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'))
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'))
authKey []byte
authKeyFile = "authKey.dat"
)
@@ -38,13 +32,6 @@ 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"`
@@ -119,70 +106,35 @@ func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
json.NewEncoder(w).Encode(c)
}
func createTcpHandler(u *url.URL) http.Handler {
u.Scheme = "http";
func createTcpHandler(e string) http.Handler {
u, err := url.Parse(e)
if err != nil {
log.Fatal(err)
}
return httputil.NewSingleHostReverseProxy(u)
}
func createTlsConfig(tlsFlags TlsFlags) *tls.Config {
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
if err != nil {
log.Fatal(err)
}
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, tlsFlags TlsFlags) http.Handler {
func createHandler(dir string, d string, e string, c Config) http.Handler {
var (
mux = http.NewServeMux()
fileHandler = http.FileServer(http.Dir(dir))
h http.Handler
)
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 strings.Contains(e, "http") {
h = createTcpHandler(e)
} else {
if _, err := os.Stat(e); err != nil {
if os.IsNotExist(err) {
log.Fatalf("unix socket %s does not exist", socketPath)
log.Fatalf("unix socket %s does not exist", e)
}
log.Fatal(err)
}
h = createUnixHandler(socketPath)
} else {
log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e)
h = createUnixHandler(e)
}
// Use existing csrf authKey if present or generate a new one.
@@ -221,7 +173,7 @@ func csrfWrapper(h http.Handler) http.Handler {
}
func main() {
kingpin.Version("1.4.0")
kingpin.Version("1.2.0")
kingpin.Parse()
configuration := Config{
@@ -229,14 +181,7 @@ func main() {
HiddenLabels: *labels,
}
tlsFlags := TlsFlags{
tls: *tlsverify,
caPath: *tlscacert,
certPath: *tlscert,
keyPath: *tlskey,
}
handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags)
handler := createHandler(*assets, *data, *endpoint, configuration)
if err := http.ListenAndServe(*addr, handler); err != nil {
log.Fatal(err)
}
+2 -13
View File
@@ -40,7 +40,6 @@ 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)
@@ -70,6 +69,7 @@ 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,10 +225,6 @@ 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: {
@@ -272,14 +268,7 @@ module.exports = function (grunt) {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'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'
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.11:4000 --swarm -d /data'
].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.4.0",
"version": "1.2.0",
"repository": {
"type": "git",
"url": "git@github.com:kevana/ui-for-docker.git"