Compare commits

...

65 Commits

Author SHA1 Message Date
Anthony Lapenna
2199c0fa06 chore(version): bump version number 2016-09-07 18:32:58 +12:00
Anthony Lapenna
719558171f Merge branch 'fix185-volume-deletion-error' into internal 2016-09-07 18:23:53 +12:00
Anthony Lapenna
65bfe93faa Merge branch 'fix193-image-error-message' into internal 2016-09-07 18:06:23 +12:00
Anthony Lapenna
f6a5c321d2 Merge branch 'bug198-hidden-containers' into internal 2016-09-07 16:43:39 +12:00
Anthony Lapenna
0570dc6d55 Merge branch 'style201-latest-logo' into internal 2016-09-07 16:26:52 +12:00
Anthony Lapenna
21b82c3758 chore(version): bump version number 2016-09-04 15:12:13 +12:00
Anthony Lapenna
a8173749c7 fix(lint): fix linting issue 2016-09-04 15:07:00 +12:00
Anthony Lapenna
9dcc61a857 Merge branch 'refactor153-rename-to-portainer' into internal 2016-09-04 15:05:38 +12:00
Anthony Lapenna
d00a24f9cd Merge branch 'docs95-docker112-support' into internal 2016-09-04 12:11:09 +12:00
Anthony Lapenna
4192920537 Merge branch 'feat95-exception-mgmt' into internal 2016-09-02 17:58:03 +12:00
Anthony Lapenna
334d91ebe8 Merge branch 'feat95-remove-errormsgfilter' into internal 2016-09-02 15:32:20 +12:00
Anthony Lapenna
3aabf7bdb7 Merge branch 'feat95-container-start-no-hostconfig' into internal 2016-09-02 13:53:35 +12:00
Anthony Lapenna
d05ac1858e Merge branch 'feat95-responsehandler-generic-handler' into internal 2016-09-01 15:09:30 +12:00
Anthony Lapenna
77aabb885c Merge branch 'feat95-responsehandler-image-delete' into internal 2016-09-01 14:28:50 +12:00
Anthony Lapenna
f76bba749e Merge branch 'feat-responsehandler-delete-network' into internal 2016-09-01 12:22:40 +12:00
Anthony Lapenna
fdeb4d4aeb Merge branch 'feat176-network-error-message' into internal 2016-09-01 11:35:38 +12:00
Anthony Lapenna
5c7d1c11b6 Merge branch 'refactor168-code-refactor' into internal 2016-08-31 18:15:39 +12:00
Anthony Lapenna
228db09ed1 Merge branch 'fix166-image-message' into internal 2016-08-31 11:27:00 +12:00
Anthony Lapenna
3f4c1f5b5d fix(image): fix the deleteImageHandler so that messages are correctly displayed in the UI 2016-08-31 11:24:45 +12:00
Anthony Lapenna
ad4c04e509 Merge branch 'fix-templates-hide-containers' into internal 2016-08-24 20:27:11 +12:00
Anthony Lapenna
382ca3135c fix(templates): hide hidden containers in templates 2016-08-24 20:26:06 +12:00
Anthony Lapenna
69ea9081dc Merge branch 'refactor-template-default-to-set' into internal 2016-08-24 19:50:00 +12:00
Anthony Lapenna
c1bd463d0b refactor(templates): use the set field instead of default 2016-08-24 19:45:54 +12:00
Anthony Lapenna
b82bbe6116 Merge branch 'feat158-templates-dropdown-containers' into internal 2016-08-24 18:35:16 +12:00
Anthony Lapenna
265b4717f6 feat(templates): support select for env fields with type 'container' 2016-08-24 18:30:56 +12:00
Anthony Lapenna
18b2612dfc Merge branch 'style160-rename-apps' into internal 2016-08-24 15:46:37 +12:00
Anthony Lapenna
423151d3c9 style(template): Update title and section name 2016-08-24 15:44:40 +12:00
Anthony Lapenna
cc9930bc4f Merge branch 'feat159-templates-env-default' into internal 2016-08-24 15:32:04 +12:00
Anthony Lapenna
161cbbd59e feat(templates): support env variables with default value 2016-08-24 15:29:50 +12:00
Anthony Lapenna
290e9402d2 refactor(lint): fix lint issue 2016-08-24 11:05:41 +12:00
Anthony Lapenna
dc811ee719 Merge branch 'feat152-container-exposed-ports-shortcut' into internal 2016-08-24 11:00:15 +12:00
Anthony Lapenna
392d4da8b0 feat(containers): update the containers view to add a column with exposed ports 2016-08-24 10:54:56 +12:00
Anthony Lapenna
4e1c2c1d33 Merge branch 'refactor-template-fields' into internal 2016-08-23 18:51:50 +12:00
Anthony Lapenna
9de537600e refactor(templates): rename field comment to description 2016-08-23 18:48:33 +12:00
Anthony Lapenna
d3eb216d9e Merge branch 'feat55-container-templates' into internal 2016-08-23 18:45:21 +12:00
Anthony Lapenna
f5bb07f062 feat(global): add templates support ('apps') 2016-08-23 17:51:12 +12:00
Anthony Lapenna
37bfe3aad0 Merge branch 'feat146-container-view' into internal 2016-08-19 18:51:05 +12:00
Anthony Lapenna
011ac4f268 Merge branch 'fix148-image-deletion' into internal 2016-08-19 17:54:34 +12:00
Anthony Lapenna
aa27c8e7cd refactor(images-view): remove useless logging statement 2016-08-19 17:52:47 +12:00
Anthony Lapenna
de7ac9708f fix(image): display a valid error message when deleting an image 2016-08-19 17:51:18 +12:00
Anthony Lapenna
a6d705fe18 feat(container): container view overhaul 2016-08-19 17:28:25 +12:00
Anthony Lapenna
01ba65a1f5 Merge branch 'feat143-host-column-name' into internal 2016-08-18 16:55:44 +12:00
Anthony Lapenna
1f26ecece9 feat(containers): rename the column header Host to Host IP 2016-08-18 16:52:46 +12:00
Anthony Lapenna
0aa84422da chore(version): bump version number 2016-08-18 15:49:18 +12:00
Anthony Lapenna
f744b4c234 fix(jshint): fix lint issue 2016-08-18 13:37:10 +12:00
Anthony Lapenna
c2ffb73358 feat(image): use a dropdown box to list available registries when tagging an image 2016-08-18 08:51:35 +12:00
Anthony Lapenna
ada649b05a Merge branch 'docs140-logo-flag' into internal 2016-08-17 21:53:36 +12:00
Anthony Lapenna
7e5a9313a0 docs(logo): add documentation about the --logo flag 2016-08-17 21:51:39 +12:00
Anthony Lapenna
62eda46bac Revert "refactor(container-creation): remove changes related to internal version"
This reverts commit 61d7b4f64c.
2016-08-17 19:35:41 +12:00
Anthony Lapenna
76c179948e Merge branch 'fix122-invalid-port-specification' into internal 2016-08-17 19:35:38 +12:00
Anthony Lapenna
ff04044001 fix(container-creation): allow to specify an address in the host port binding 2016-08-17 19:30:21 +12:00
Anthony Lapenna
a752e8bc54 Merge branch 'fix123-container-creation-missing-port-bindings' into internal 2016-08-17 19:06:17 +12:00
Anthony Lapenna
f14cbc5967 fix(container-creation): fix unregistered ports bindings when creating a container 2016-08-17 18:57:38 +12:00
Anthony Lapenna
7ebf842601 Merge branch 'docs-reverse-proxy' into internal 2016-08-17 18:32:26 +12:00
Anthony Lapenna
8dcac532d6 docs(reverse-proxy): add reverse proxy instructions 2016-08-17 18:30:21 +12:00
Anthony Lapenna
39d3753acb Merge branch 'feat111-exec-protocol-awareness' into internal 2016-08-17 18:24:10 +12:00
Anthony Lapenna
1e13aa660e feat(console): add protocol awareness to switch between ws:// and wss:// 2016-08-17 18:22:31 +12:00
Anthony Lapenna
0bd599f98a fix(api): remove git conflict trace 2016-08-17 18:16:01 +12:00
Anthony Lapenna
f6e05538d8 Revert "refactor(global): revert merge with internal (#133)"
This reverts commit eefa7ca138.
2016-08-17 18:09:55 +12:00
Anthony Lapenna
0acbc10095 Merge branch 'feat125-tag-image' into internal 2016-08-17 18:06:09 +12:00
Anthony Lapenna
aa1017c260 feat(image): add the ability to tag an image 2016-08-17 18:04:29 +12:00
Anthony Lapenna
9cb7483e0a Merge branch 'refactor-remove-binary' into internal 2016-08-17 13:58:15 +12:00
Anthony Lapenna
7d4ae1a6fd refactor(api): remove the binary from versioning 2016-08-17 13:56:09 +12:00
Anthony Lapenna
fa5c1d16b6 Merge branch 'refactor124-response-handlers' into internal 2016-08-17 13:53:57 +12:00
Anthony Lapenna
c928af71a4 refactor(handlers): remove duplicated code 2016-08-17 13:42:33 +12:00
23 changed files with 279 additions and 408 deletions

View File

@@ -109,6 +109,16 @@ 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/portainer -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
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
@@ -156,6 +166,7 @@ The following options are available for the `portainer` binary:
* `--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`)

View File

@@ -6,20 +6,21 @@ import (
// main is the entry point of the program
func main() {
kingpin.Version("1.8.0")
kingpin.Version("1.8.1")
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 Portainer").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()
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()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String()
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve Portainer").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()
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()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String()
)
kingpin.Parse()
@@ -39,6 +40,7 @@ func main() {
settings := &Settings{
Swarm: *swarm,
HiddenLabels: *labels,
Registries: *registries,
Logo: *logo,
}

View File

@@ -9,6 +9,7 @@ import (
type Settings struct {
Swarm bool `json:"swarm"`
HiddenLabels pairList `json:"hiddenLabels"`
Registries pairList `json:"registries"`
Logo string `json:"logo"`
}

View File

@@ -22,12 +22,10 @@ angular.module('portainer', [
'swarm',
'network',
'networks',
'createNetwork',
'templates',
'volumes',
'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
'templates',
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
@@ -164,4 +162,4 @@ angular.module('portainer', [
.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', 'settings')
.constant('TEMPLATES_ENDPOINT', 'templates')
.constant('UI_VERSION', 'v1.8.0');
.constant('UI_VERSION', 'v1.8.1');

View File

@@ -104,7 +104,10 @@
</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="optional">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->

View File

@@ -1,12 +1,15 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) {
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Config', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Container, ContainerCommit, ImageHelper, Messages) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.formValues = {
AvailableRegistries: []
};
var update = function () {
$('#loadingViewSpinner').show();
@@ -153,5 +156,8 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false;
};
update();
Config.$promise.then(function (c) {
$scope.formValues.AvailableRegistries = c.registries;
update();
});
}]);

View File

@@ -59,7 +59,12 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
$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, "Unable to retrieve volumes");
});

View File

@@ -26,7 +26,10 @@
</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">
<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">
@@ -107,6 +110,7 @@
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
@@ -211,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>

View File

@@ -1,74 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$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.message) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', {}, d.message);
} else {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
}
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);
};
}]);

View File

@@ -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>

View File

@@ -1,56 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$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.message) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
}
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);
};
}]);

View File

@@ -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>

View File

@@ -48,7 +48,10 @@
</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="optional">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->

View File

@@ -1,12 +1,16 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Config', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Config, Image, ImageHelper, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.formValues = {
AvailableRegistries: []
};
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
@@ -72,18 +76,22 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
});
};
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve image info");
Config.$promise.then(function (c) {
$scope.formValues.AvailableRegistries = c.registries;
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve image info");
});
});
}]);

View File

@@ -22,7 +22,10 @@
</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">
<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-and-registry-inputs -->
@@ -88,13 +91,6 @@
<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>
@@ -105,12 +101,11 @@
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getisodatefromtimestamp }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
<rd-widget>
</div>
</div>

View File

@@ -40,7 +40,7 @@ function ($scope, $state, Config, Image, Messages) {
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
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');

View File

@@ -7,6 +7,64 @@
<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 -->
<!-- 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" ng-if="swarm">
<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">
@@ -17,7 +75,6 @@
<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 +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>
@@ -84,10 +120,7 @@
<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:40}}</a></td>
<td>{{ network.Id }}</td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</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>

View File

@@ -4,7 +4,7 @@ function ($scope, $state, Network, Config, Messages) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.formValues = {
@@ -32,6 +32,44 @@ function ($scope, $state, Network, Config, Messages) {
}
};
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);
if ($scope.swarm) {
config.Driver = 'overlay';
}
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.message) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', {}, d.message);
} else {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
var counter = 0;
@@ -72,5 +110,8 @@ function ($scope, $state, Network, Config, Messages) {
});
}
fetchNetworks();
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchNetworks();
});
}]);

View File

@@ -127,6 +127,19 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
return volumeQueries;
}
function generateUUID(){
var d = moment().millisecond();
if(window.performance && typeof window.performance.now === "function"){
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c==='x' ? r : (r&0x3|0x8)).toString(16);
});
return uuid;
}
$scope.createTemplate = function() {
$('#createContainerSpinner').show();
var template = $scope.selectedTemplate;

View File

@@ -7,6 +7,40 @@
<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">
@@ -17,7 +51,6 @@
<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,13 +76,6 @@
<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>
@@ -57,11 +83,10 @@
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
<rd-widget>
</div>

View File

@@ -3,7 +3,7 @@ angular.module('volumes', [])
function ($scope, $state, Volume, Messages) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortType = 'Driver';
$scope.sortReverse = true;
$scope.config = {
@@ -23,6 +23,32 @@ function ($scope, $state, Volume, Messages) {
}
};
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.message) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
};
$scope.removeAction = function () {
$('#loadVolumesSpinner').show();
var counter = 0;
@@ -55,7 +81,7 @@ function ($scope, $state, Volume, Messages) {
function fetchVolumes() {
$('#loadVolumesSpinner').show();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes;
$scope.volumes = _.uniqBy(d.Volumes, 'Name');
$('#loadVolumesSpinner').hide();
}, function (e) {
$('#loadVolumesSpinner').hide();

View File

@@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.8.0",
"version": "1.8.1",
"homepage": "https://github.com/cloud-inovasi/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>"

View File

@@ -2,7 +2,7 @@
"author": "Cloud Inovasi",
"name": "portainer",
"homepage": "https://github.com/cloud-inovasi/portainer",
"version": "1.8.0",
"version": "1.8.1",
"repository": {
"type": "git",
"url": "git@github.com:cloud-inovasi/portainer.git"