diff --git a/README.md b/README.md index 89a0c7d31..f7d63d6a5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https: UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker. ## Goals + * 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. @@ -14,7 +15,7 @@ UI For Docker is a web interface for the Docker Remote API. The goal is to prov 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. +At the moment, the following versions are supported: 1.9, 1.10 & 1.11. ## Run @@ -83,9 +84,21 @@ $ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/cer *Note*: Replace `/path/to/certs` to the path to the certificate files on your disk. +### Use your own logo + +You can use the `--logo` flag to specify an URL to your own logo. + +For example, using the Docker logo: + +``` +$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg" +``` + +The custom logo will replace the CloudInovasi logo in the UI. + ### Hide containers with specific labels -You can hide specific containers in the containers view by using the `-hide-label` or `-l` options and specifying a label. +You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label. For example, take a container started with the label `owner=acme`: @@ -99,6 +112,36 @@ You can hide it in the view by starting the ui with: $ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme ``` +### Reverse proxy configuration + +Has been tested with Nginx 1.11. + +Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`: + +```nginx +upstream cloudinovasi-ui { + server ADDRESS:PORT; +} + +server { + listen 80; + + location /dockerui/ { + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_pass http://cloudinovasi-ui/; + } + location /dockerui/ws/ { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + proxy_pass http://cloudinovasi-ui/ws/; + } +} +``` + +Replace `ADDRESS:PORT` with the CloudInovasi UI container details. + ### Available options The following options are available for the `ui-for-docker` binary: @@ -108,8 +151,9 @@ The following options are available for the `ui-for-docker` binary: * `--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`) +* `--hide-label`, `-l`: Hide containers with a specific label in the UI +* `--logo`: URL to a picture to be displayed as a logo in the UI diff --git a/api/main.go b/api/main.go index a2c5a0f15..877271926 100644 --- a/api/main.go +++ b/api/main.go @@ -6,7 +6,7 @@ import ( // main is the entry point of the program func main() { - kingpin.Version("1.6.0") + kingpin.Version("1.7.0") 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() @@ -18,6 +18,7 @@ func main() { 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() ) kingpin.Parse() @@ -36,6 +37,7 @@ func main() { settings := &Settings{ Swarm: *swarm, HiddenLabels: *labels, + Logo: *logo, } api := newAPI(apiConfig) diff --git a/api/settings.go b/api/settings.go index 801904f61..b707a8e69 100644 --- a/api/settings.go +++ b/api/settings.go @@ -9,6 +9,7 @@ import ( type Settings struct { Swarm bool `json:"swarm"` HiddenLabels pairList `json:"hiddenLabels"` + Logo string `json:"logo"` } // configurationHandler defines a handler function used to encode the configuration in JSON diff --git a/app/app.js b/app/app.js index 206161c75..9cb8457d7 100644 --- a/app/app.js +++ b/app/app.js @@ -155,5 +155,5 @@ angular.module('uifordocker', [ // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 .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', 'settings') - .constant('UI_VERSION', 'v1.6.0'); + .constant('CONFIG_ENDPOINT', 'settings') + .constant('UI_VERSION', 'v1.7.0'); diff --git a/app/components/container/container.html b/app/components/container/container.html index bc76f31cc..52e203069 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -84,7 +84,7 @@ Created - {{ container.Created | date: 'medium' }} + {{ container.Created|getisodate }} Path @@ -186,7 +186,8 @@ {{key}} - {{ val }} + {{val|getisodate}} + {{val}} diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js index 566529458..644e5f3dd 100644 --- a/app/components/containerConsole/containerConsoleController.js +++ b/app/components/containerConsole/containerConsoleController.js @@ -36,7 +36,12 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, e if (d.Id) { var execId = d.Id; resizeTTY(execId, termHeight, termWidth); - var url = window.location.href.split('#')[0].replace('http://', 'ws://') + 'ws/exec?id=' + execId; + var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId; + if (url.indexOf('https') > -1) { + url = url.replace('https://', 'wss://'); + } else { + url = url.replace('http://', 'ws://'); + } initTerm(url, termHeight, termWidth); } else { $('#loadConsoleSpinner').hide(); diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 0a7ca115a..844d247ea 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -89,7 +89,7 @@ {{ container|swarmcontainername}} {{ container|containername}} {{ container.IP ? container.IP : '-' }} - {{ container|swarmhostname}} + {{ container.hostIP }} {{ container.Image }} {{ container.Command|truncate:60 }} diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index b7662e243..0db7b4dc8 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -1,6 +1,6 @@ angular.module('containers', []) -.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'Config', 'errorMsgFilter', -function ($scope, Container, Settings, Messages, Config, errorMsgFilter) { +.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter', +function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) { $scope.state = {}; $scope.state.displayAll = Settings.displayAll; @@ -27,6 +27,9 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) { if (model.IP) { $scope.state.displayIP = true; } + if ($scope.swarm) { + model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]]; + } return model; }); $('#loadContainersSpinner').hide(); @@ -154,10 +157,32 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) { }); }; + function retrieveSwarmHostsInfo(data) { + var swarm_hosts = {}; + var systemStatus = data.SystemStatus; + var node_count = parseInt(systemStatus[3][1], 10); + var node_offset = 4; + for (i = 0; i < node_count; i++) { + var host = {}; + host.name = _.trim(systemStatus[node_offset][0]); + host.ip = _.split(systemStatus[node_offset][1], ':')[0]; + swarm_hosts[host.name] = host.ip; + node_offset += 9; + } + return swarm_hosts; + } + $scope.swarm = false; Config.$promise.then(function (c) { hiddenLabels = c.hiddenLabels; $scope.swarm = c.swarm; - update({all: Settings.displayAll ? 1 : 0}); + if (c.swarm) { + Info.get({}, function (d) { + $scope.swarm_hosts = retrieveSwarmHostsInfo(d); + update({all: Settings.displayAll ? 1 : 0}); + }); + } else { + update({all: Settings.displayAll ? 1 : 0}); + } }); }]); diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 7a761c04d..623da4eb8 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -9,6 +9,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e $scope.formValues = { Console: 'none', Volumes: [], + AvailableRegistries: [], Registry: '' }; @@ -16,6 +17,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e $scope.config = { Env: [], + ExposedPorts: {}, HostConfig: { RestartPolicy: { Name: 'no' @@ -27,12 +29,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) { @@ -58,6 +56,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e Config.$promise.then(function (c) { var swarm = c.swarm; + $scope.formValues.AvailableRegistries = c.registries; + Volume.query({}, function (d) { $scope.availableVolumes = d.Volumes; }, function (e) { @@ -72,6 +72,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"}); @@ -149,7 +150,16 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e config.HostConfig.PortBindings.forEach(function (portBinding) { if (portBinding.hostPort && portBinding.containerPort) { var key = portBinding.containerPort + "/" + portBinding.protocol; - bindings[key] = [{ HostPort: portBinding.hostPort }]; + var binding = {}; + if (portBinding.hostPort.indexOf(':') > -1) { + var hostAndPort = portBinding.hostPort.split(':'); + binding.HostIp = hostAndPort[0]; + binding.HostPort = hostAndPort[1]; + } else { + binding.HostPort = portBinding.hostPort; + } + bindings[key] = [binding]; + config.ExposedPorts[key] = {}; } }); config.HostConfig.PortBindings = bindings; diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index c6bd3d04f..3eb25b6f0 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -107,7 +107,6 @@
  • Network
  • Security/Host
  • -
    @@ -255,6 +254,11 @@
    +
    +
    + You don't have any shared network. Head over the networks view to create one. +
    +
    diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index 2b3667304..0368f3ee2 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -50,7 +50,9 @@ function ($scope, $q, Config, Container, Image, Network, Volume, Info) { function prepareVolumeData(d) { var volumes = d.Volumes; - $scope.volumeData.total = volumes.length; + if (volumes) { + $scope.volumeData.total = volumes.length; + } } function prepareNetworkData(d) { diff --git a/app/components/events/events.html b/app/components/events/events.html index 17764e1fb..f68971578 100644 --- a/app/components/events/events.html +++ b/app/components/events/events.html @@ -50,7 +50,7 @@ - {{ event.Time|getdatefromtimestamp }} + {{ event.Time|getisodatefromtimestamp }} {{ event.Type }} {{ event.Details }} diff --git a/app/components/image/image.html b/app/components/image/image.html index eeff8d7eb..b2474d954 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -1,97 +1,159 @@ - + + + - Images > {{ id }} + Images > {{ image.Id }} +
    +
    + + + +
    + + + + + {{ tag }} + + + + +
    +
    + + Note: you can click on the upload icon to push an image + and on the trash icon to delete a tag + +
    +
    +
    +
    +
    + -
    - -
    -
    {{ id }}
    -
    Image ID
    + + +
    + +
    + +
    + +
    + +
    +
    + + +
    +
    + Note: if you don't specify the tag in the image name, latest will be used. +
    +
    + +
    +
    + + +
    +
    +
    +
    - - -
    - -
    -
    -
    - -
    -
    -
    - Actions -
    -
    -
    -
    -
    -
    -
    - - - - - + - + - - - - - - - + + - - + + - - - - - - - - - - - - - + + + + + + +
    Created{{ image.Created | date: 'medium'}}
    TagsID -
      -
    • {{ tag }} - -
    • -
    + {{ image.Id }} +
    Parent {{ image.Parent }}
    Size (Virtual Size){{ image.Size|humansize }} ({{ image.VirtualSize|humansize }})
    Hostname{{ image.ContainerConfig.Hostname }}Size{{ image.VirtualSize|humansize }}
    User{{ image.ContainerConfig.User }}Created{{ image.Created|getisodate }}
    Cmd{{ image.ContainerConfig.Cmd }}
    Volumes{{ image.ContainerConfig.Volumes }}
    Volumes from{{ image.ContainerConfig.VolumesFrom }}
    Built withBuild Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }}
    Author{{ image.Author }}
    +
    +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    CMD{{ image.ContainerConfig.Cmd|command }}
    ENTRYPOINT{{ image.ContainerConfig.Entrypoint|command }}
    EXPOSE + + {{ port }} + +
    VOLUME + + {{ volume }} + +
    ENV + + + + + +
    {{ var|key }}{{ var|value }}
    +
    diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index e1710a62f..8d2b4c598 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,33 +1,15 @@ angular.module('image', []) -.controller('ImageController', ['$scope', '$q', '$stateParams', '$state', 'Image', 'Container', 'Messages', 'LineChart', -function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChart) { - $scope.tagInfo = {repo: '', version: '', force: false}; - $scope.id = ''; - $scope.repoTags = []; +.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'Messages', +function ($scope, $stateParams, $state, Image, Messages) { + $scope.RepoTags = []; - $scope.removeImage = function (id) { - Image.remove({id: id}, function (d) { - d.forEach(function(msg){ - var key = Object.keys(msg)[0]; - Messages.send(key, msg[key]); - }); - // If last message key is 'Deleted' then assume the image is gone and send to images page - if (d[d.length-1].Deleted) { - $state.go('images', {}, {reload: true}); - } else { - $state.go('image', {id: $scope.id}, {reload: true}); - } - }, function (e) { - $scope.error = e.data; - $('#error-message').show(); - }); + $scope.config = { + Image: '', + Registry: '' }; - /** - * Get RepoTags from the /images/query endpoint instead of /image/json, - * for backwards compatibility with Docker API versions older than 1.21 - * @param {string} imageId - */ + // 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) { Image.query({}, function (d) { d.forEach(function(image) { @@ -38,21 +20,88 @@ function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChar }); } + function createImageConfig(imageName, registry) { + var imageNameAndTag = imageName.split(':'); + var image = imageNameAndTag[0]; + if (registry) { + image = registry + '/' + imageNameAndTag[0]; + } + var imageConfig = { + repo: image, + tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest' + }; + return imageConfig; + } + + $scope.tagImage = function() { + $('#loadingViewSpinner').show(); + var image = _.toLower($scope.config.Image); + var registry = _.toLower($scope.config.Registry); + var imageConfig = createImageConfig(image, registry); + Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { + Messages.send('Image successfully tagged'); + $('#loadingViewSpinner').hide(); + $state.go('image', {id: $stateParams.id}, {reload: true}); + }, function(e) { + $('#loadingViewSpinner').hide(); + Messages.error("Unable to tag image", e.data); + }); + }; + + $scope.pushImage = function(tag) { + $('#loadingViewSpinner').show(); + Image.push({tag: tag}, function (d) { + if (d[d.length-1].error) { + Messages.error("Unable to push image", d[d.length-1].error); + } else { + Messages.send('Image successfully pushed'); + } + $('#loadingViewSpinner').hide(); + }, function (e) { + $('#loadingViewSpinner').hide(); + Messages.error("Unable to push image", e.data); + }); + }; + + $scope.removeImage = function (id) { + $('#loadingViewSpinner').show(); + Image.remove({id: id}, function (d) { + if (d[0].message) { + $('#loadingViewSpinner').hide(); + Messages.error("Unable to remove image", d[0].message); + } else { + // If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image + // then assume the image is gone and send to images page + if (d[d.length-1].Deleted || (d[d.length-1].Untagged && $scope.RepoTags.length === 1)) { + Messages.send('Image successfully deleted'); + $state.go('images', {}, {reload: true}); + } else { + Messages.send('Tag successfully deleted'); + $state.go('image', {id: $stateParams.id}, {reload: true}); + } + } + }, function (e) { + $('#loadingViewSpinner').hide(); + Messages.error("Unable to remove image", e.data); + }); + }; + + $('#loadingViewSpinner').show(); Image.get({id: $stateParams.id}, function (d) { $scope.image = d; - $scope.id = d.Id; if (d.RepoTags) { $scope.RepoTags = d.RepoTags; } else { - getRepoTags($scope.id); + 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) { if (e.status === 404) { - $('.detail').hide(); - $scope.error = "Image not found.
    " + $stateParams.id; + Messages.error("Unable to find image", $stateParams.id); } else { - $scope.error = e.data; + Messages.error("Unable to retrieve image info", e.data); } - $('#error-message').show(); }); }]); diff --git a/app/components/images/images.html b/app/components/images/images.html index c44f1a431..f4bf6fd4e 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -66,7 +66,7 @@ - + - +
    Id @@ -83,7 +83,7 @@ - VirtualSize + Size @@ -105,12 +105,12 @@ {{ tag }} {{ image.VirtualSize|humansize }}{{ image.Created|getdate }}{{ image.Created|getisodatefromtimestamp }}
    - -
    -
    + +
    +
    diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index 5eac079dd..d31cbb01f 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -1,10 +1,9 @@ angular.module('images', []) -.controller('ImagesController', ['$scope', '$state', 'Image', 'Messages', -function ($scope, $state, Image, Messages) { +.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages', +function ($scope, $state, Config, Image, Messages) { $scope.state = {}; - $scope.sortType = 'Created'; + $scope.sortType = 'RepoTags'; $scope.sortReverse = true; - $scope.state.toggle = false; $scope.state.selectedItemCount = 0; $scope.config = { @@ -17,17 +16,6 @@ function ($scope, $state, Image, Messages) { $scope.sortType = sortType; }; - $scope.toggleSelectAll = function () { - angular.forEach($scope.state.filteredImages, function (i) { - i.Checked = $scope.state.toggle; - }); - if ($scope.state.toggle) { - $scope.state.selectedItemCount = $scope.state.filteredImages.length; - } else { - $scope.state.selectedItemCount = 0; - } - }; - $scope.selectItem = function (item) { if (item.Checked) { $scope.state.selectedItemCount++; @@ -83,15 +71,17 @@ function ($scope, $state, Image, Messages) { if (i.Checked) { counter = counter + 1; Image.remove({id: i.Id}, function (d) { - angular.forEach(d, function (resource) { - Messages.send("Image deleted", resource.Deleted); - }); - var index = $scope.images.indexOf(i); - $scope.images.splice(index, 1); + if (d[0].message) { + $('#loadingViewSpinner').hide(); + Messages.error("Unable to remove image", d[0].message); + } else { + Messages.send("Image deleted", i.Id); + var index = $scope.images.indexOf(i); + $scope.images.splice(index, 1); + } complete(); }, function (e) { - Messages.error("Failure", e.data); - $('#loadImagesSpinner').hide(); + Messages.error("Unable to remove image", e.data); complete(); }); } @@ -110,5 +100,9 @@ function ($scope, $state, Image, Messages) { }); } - fetchImages(); + Config.$promise.then(function (c) { + $scope.availableRegistries = c.registries; + fetchImages(); + }); + }]); diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index 96add077d..aba75e57f 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -28,7 +28,7 @@ - + - + - - + +
    Name @@ -83,13 +83,13 @@
    {{ network.Name|truncate:20}}{{ network.Name|truncate:40}} {{ network.Id }} {{ network.Scope }} {{ network.Driver }} {{ network.IPAM.Driver }}{{ network.IPAM.Config[0].Subnet }}{{ network.IPAM.Config[0].Gateway }}{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}
    diff --git a/app/components/networks/networksController.js b/app/components/networks/networksController.js index 3cde28bfa..79347b6e1 100644 --- a/app/components/networks/networksController.js +++ b/app/components/networks/networksController.js @@ -1,29 +1,29 @@ angular.module('networks', []) -.controller('NetworksController', ['$scope', 'Network', 'Messages', 'errorMsgFilter', -function ($scope, Network, Messages, errorMsgFilter) { - +.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter', +function ($scope, $state, Network, Config, Messages, errorMsgFilter) { $scope.state = {}; - $scope.state.toggle = false; $scope.state.selectedItemCount = 0; + $scope.state.advancedSettings = false; $scope.sortType = 'Name'; - $scope.sortReverse = true; + $scope.sortReverse = false; + + $scope.formValues = { + Subnet: '', + Gateway: '' + }; + + $scope.config = { + Name: '', + IPAM: { + Config: [] + } + }; $scope.order = function(sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; - $scope.toggleSelectAll = function () { - angular.forEach($scope.state.filteredNetworks, function (i) { - i.Checked = $scope.state.toggle; - }); - if ($scope.state.toggle) { - $scope.state.selectedItemCount = $scope.state.filteredNetworks.length; - } else { - $scope.state.selectedItemCount = 0; - } - }; - $scope.selectItem = function (item) { if (item.Checked) { $scope.state.selectedItemCount++; @@ -72,5 +72,6 @@ function ($scope, Network, Messages, errorMsgFilter) { $('#loadNetworksSpinner').hide(); }); } + fetchNetworks(); }]); diff --git a/app/components/volumes/volumes.html b/app/components/volumes/volumes.html index 6c66bc2f0..869ec72b4 100644 --- a/app/components/volumes/volumes.html +++ b/app/components/volumes/volumes.html @@ -28,7 +28,7 @@ - + - + @@ -63,5 +63,5 @@
    Name @@ -55,7 +55,7 @@
    {{ volume.Name|truncate:20 }}{{ volume.Name|truncate:50 }} {{ volume.Driver }} {{ volume.Mountpoint }}
    - - + + diff --git a/app/components/volumes/volumesController.js b/app/components/volumes/volumesController.js index 4b684e60c..cad6790e3 100644 --- a/app/components/volumes/volumesController.js +++ b/app/components/volumes/volumesController.js @@ -1,28 +1,20 @@ angular.module('volumes', []) -.controller('VolumesController', ['$scope', 'Volume', 'Messages', 'errorMsgFilter', -function ($scope, Volume, Messages, errorMsgFilter) { +.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter', +function ($scope, $state, Volume, Messages, errorMsgFilter) { $scope.state = {}; - $scope.state.toggle = false; $scope.state.selectedItemCount = 0; $scope.sortType = 'Name'; $scope.sortReverse = true; + $scope.config = { + Name: '' + }; + $scope.order = function(sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; - $scope.toggleSelectAll = function () { - angular.forEach($scope.state.filteredVolumes, function (i) { - i.Checked = $scope.state.toggle; - }); - if ($scope.state.toggle) { - $scope.state.selectedItemCount = $scope.state.filteredVolumes.length; - } else { - $scope.state.selectedItemCount = 0; - } - }; - $scope.selectItem = function (item) { if (item.Checked) { $scope.state.selectedItemCount++; diff --git a/app/shared/filters.js b/app/shared/filters.js index ae0cb58b1..a0afbcb41 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -94,7 +94,6 @@ angular.module('uifordocker.filters', []) if (state === undefined) { return 'label-default'; } - if (state.Ghost && state.Running) { return 'label-important'; } @@ -155,20 +154,38 @@ angular.module('uifordocker.filters', []) return []; }; }) -.filter('getdate', function () { - 'use strict'; - return function (data) { - //Multiply by 1000 for the unix format - var date = new Date(data * 1000); - return date.toDateString(); - }; -}) -.filter('getdatefromtimestamp', function () { +.filter('getisodatefromtimestamp', function () { 'use strict'; return function (timestamp) { return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); }; }) +.filter('getisodate', function () { + 'use strict'; + return function (date) { + return moment(date).format('YYYY-MM-DD HH:mm:ss'); + }; +}) +.filter('command', function () { + 'use strict'; + return function (command) { + if (command) { + return command.join(' '); + } + }; +}) +.filter('key', function () { + 'use strict'; + return function (pair) { + return pair.slice(0, pair.indexOf('=')); + }; +}) +.filter('value', function () { + 'use strict'; + return function (pair) { + return pair.slice(pair.indexOf('=') + 1); + }; +}) .filter('errorMsg', function () { return function (object) { var idx = 0; diff --git a/app/shared/responseHandlers.js b/app/shared/responseHandlers.js new file mode 100644 index 000000000..4625ba1fc --- /dev/null +++ b/app/shared/responseHandlers.js @@ -0,0 +1,19 @@ +// The Docker API often returns a list of JSON object. +// This handler wrap the JSON objects in an array. +// Used by the API in: Image push, Image create, Events query. +function jsonObjectsToArrayHandler(data) { + var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]"; + return angular.fromJson(str); +} + +// Image delete API returns an array on success and an object on error. +// This handler creates an array from an object in case of error. +function deleteImageHandler(data) { + var response = angular.fromJson(data); + if (!Array.isArray(response)) { + var arr = []; + arr.push(response); + return arr; + } + return response; +} diff --git a/app/shared/services.js b/app/shared/services.js index d5cee721b..7f7cd59f6 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -92,28 +92,31 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize']) get: {method: 'GET', params: {action: 'json'}}, search: {method: 'GET', params: {action: 'search'}}, history: {method: 'GET', params: {action: 'history'}, isArray: true}, - create: { - method: 'POST', isArray: true, transformResponse: [function f(data) { - var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]"; - return angular.fromJson(str); - }], - params: {action: 'create', fromImage: '@fromImage', tag: '@tag'} - }, insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, - push: {method: 'POST', params: {id: '@id', action: 'push'}}, tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo', tag: '@tag'}}, - remove: {method: 'DELETE', params: {id: '@id'}, isArray: true}, - inspect: {method: 'GET', params: {id: '@id', action: 'json'}} + inspect: {method: 'GET', params: {id: '@id', action: 'json'}}, + push: { + method: 'POST', params: {action: 'push', id: '@tag'}, + isArray: true, transformResponse: jsonObjectsToArrayHandler + }, + create: { + method: 'POST', params: {action: 'create', fromImage: '@fromImage', tag: '@tag'}, + isArray: true, transformResponse: jsonObjectsToArrayHandler + }, + remove: { + method: 'DELETE', params: {id: '@id'}, + isArray: true, transformResponse: deleteImageHandler + } }); }]) .factory('Events', ['$resource', 'Settings', function EventFactory($resource, Settings) { 'use strict'; // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/monitor-docker-s-events return $resource(Settings.url + '/events', {}, { - query: {method: 'GET', params: {since: '@since', until: '@until'}, isArray: true, transformResponse: [function f(data) { - var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]"; - return angular.fromJson(str); - }]} + query: { + method: 'GET', params: {since: '@since', until: '@until'}, + isArray: true, transformResponse: jsonObjectsToArrayHandler + } }); }]) .factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) { diff --git a/assets/css/app.css b/assets/css/app.css index ecfd4e977..01d4780c2 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -117,7 +117,7 @@ .logo { display: inline; width: 100%; - max-width: 160px; + max-width: 155px; height: 100%; max-height: 55px; margin-bottom: 5px; @@ -170,18 +170,26 @@ input[type="radio"] { margin-right: 5px; } -.green-icon { +.fa.green-icon { color: #23ae89; } -.red-icon { +.fa.red-icon { color: #ae2323; } +.fa.white-icon { + color: white; +} + .image-tag { margin-right: 5px; } +.label.tag { + margin-right: 5px; +} + .widget .widget-body table tbody .image-tag { font-size: 90% !important; } @@ -190,3 +198,7 @@ input[type="radio"] { width: 100%; padding: 10px 5px; } + +.interactive { + cursor: pointer; +} diff --git a/bower.json b/bower.json index 8220a2dde..39825602c 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "uifordocker", - "version": "1.6.0", + "version": "1.7.0", "homepage": "https://github.com/kevana/ui-for-docker", "authors": [ "Michael Crosby ", diff --git a/index.html b/index.html index 434d0f048..389d0056e 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,8 @@