Compare commits

...

632 Commits

Author SHA1 Message Date
Anthony Lapenna 7afeb8a80d Merge branch 'release/1.11.2' 2017-01-26 17:43:53 +13:00
Anthony Lapenna f8ced03792 chore(version): bump version number 2017-01-26 17:43:47 +13:00
Jisu Park 1fdf56372b feat(containers): support container already pause message (#480) 2017-01-26 12:11:38 +13:00
Anthony Lapenna 835b273700 feat(api): force no-cache on HTML files 2017-01-26 11:45:03 +13:00
Anthony Lapenna fcc9203416 feat(node): add pagination to associated tasks 2017-01-26 10:35:05 +13:00
Anthony Lapenna e25c5a014c feat(swarm): set default sorting for Swarm nodes by role 2017-01-26 10:34:10 +13:00
Glowbal fa9ba303aa #414 feat(node-details): add ability to view and edit Swarm mode nodes (#417) 2017-01-26 10:12:04 +13:00
morph027 e6dee37af0 style(swarm): update node status filter for swarm mode nodes 2017-01-26 09:54:08 +13:00
Anthony Lapenna d03e992b4f feat(api): replace all calls to http.Error with custom Error writer 2017-01-24 16:35:48 +13:00
Anthony Lapenna 1a868be6ea fix(swarm): fix sorting issue with node table (#538) 2017-01-24 14:45:38 +13:00
Anthony Lapenna e2fc8af87a feat(ux): add the ability to change the number of paginated items on all entity tables (#537) 2017-01-24 14:28:40 +13:00
Anthony Lapenna 70933d1056 style(sidebar): add active class on Docker section (#534) 2017-01-24 09:39:13 +13:00
Anthony Lapenna 7e0b0a05de feat(authentication): clean the state and the browser local storage on logout 2017-01-23 17:04:34 +13:00
Anthony Lapenna 980f65a08a feat(api): initializes the endpoint with an empty slice instead of a pointer 2017-01-23 16:29:49 +13:00
Anthony Lapenna 8cf6d34362 style(container-creation): remove useless labels section (#532) 2017-01-23 16:10:12 +13:00
Anthony Lapenna 70f139514f fix(network-details): add a fallback for listing containers when APIV… (#531) 2017-01-23 16:06:51 +13:00
Anthony Lapenna fa4ec04c47 feat(state): introduce endpoint state (#529) 2017-01-23 12:14:34 +13:00
Anthony Lapenna 7ebe4af77d fix(images): fix an issue when deleting images with multiple tags (#526) 2017-01-22 14:42:12 +13:00
lpfeup 579241db92 #503 fix(container-stats): fix container stats timer not being properly canceled. (#504) 2017-01-21 18:04:28 +13:00
lpfeup 7d78871eee #446 fix(container-stats): fix issue in stats view with empty network data (#502) 2017-01-21 18:01:32 +13:00
Anthony Lapenna 3a6e9d2fbe fix(api): fix an issue introduced by the latest version of package gorilla/mux (#520) 2017-01-21 11:17:51 +13:00
Anthony Lapenna e4d98082dc fix(api): disable data directory creation (#495)
* fix(api): disable data directory creation

* feat(dockerhub): update volume instruction value for Windows Dockerfiles
2017-01-14 14:22:39 +13:00
Kilhog cd26051144 #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://" (#477)
* #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://"

* #477 fix(PR): Rename 'TYPE' in 'type'
2017-01-12 18:44:53 +13:00
Anthony Lapenna 27e584fc14 fix(api): check if admin user already exists when calling the /users/admin/init endpoint (#494) 2017-01-12 18:17:28 +13:00
Anthony Lapenna 2bdc9322de style(containers): update header text for published ports (#483) 2017-01-09 21:50:19 +13:00
Anthony Lapenna 35d5d75966 fix(api): update default value for data directory and TLS certs on Windows (#482) 2017-01-09 21:24:17 +13:00
Anthony Lapenna 2610e3d02a Merge tag '1.11.1' into develop
Release 1.11.1
2017-01-05 10:42:50 +13:00
Anthony Lapenna d579f62fa7 Merge branch 'release/1.11.1' 2017-01-05 10:42:46 +13:00
Anthony Lapenna d1b9820a29 chore(version): bump version number 2017-01-05 10:42:38 +13:00
Wouter Oet 13943c3d8b #372 feat(UX): Implement select all functionality (#437) 2017-01-05 09:15:41 +13:00
Anthony Lapenna d8b800ddbc feat(api): create platform dependant default values for CLI flags (#458) 2017-01-04 19:50:25 +13:00
Matthew Strickland 59f1a2f673 feat(templates): display container restart policy in container dashboard (#434) (#435) 2017-01-04 19:49:04 +13:00
Anthony Lapenna 9ee652c818 fix(api): creates the data directory if not exist (#452) 2017-01-03 08:32:53 +13:00
Anthony Lapenna 816c1ea448 chore(build-system): fix release tasks 2017-01-03 07:47:12 +13:00
Albert Domenech 0bacaef71a feat(images): initial aarch64/arm64 support (#447) 2017-01-03 07:42:21 +13:00
Anthony Lapenna 2ef821f118 style(service-details): update style for update failure action field (#443) 2016-12-31 13:32:20 +13:00
Anthony Lapenna 487cb4e755 Merge branch 'develop' of github.com:portainer/portainer into develop 2016-12-31 13:27:51 +13:00
Anthony Lapenna 06d3debf38 chore(build-system): fix grunt lint task 2016-12-31 13:27:35 +13:00
Anthony Lapenna 907f83aaff fix(global): remove automatic lowercase processing on image names (#442) 2016-12-31 13:25:42 +13:00
Gábor Kovács 4b747a78cd style(sidebar): Highlight active page in sidebar (#420)
* Issue #331

* New line
2016-12-31 13:12:51 +13:00
Anthony Lapenna d6f3dd8cda style(endpoint-initialization): update requirement message for local endpoint init (#424) 2016-12-31 13:00:30 +13:00
Anthony Lapenna 51632e367c fix(service-details): allow to specify the 0 value for replicas (#441) 2016-12-31 12:59:20 +13:00
Anthony Lapenna 6e98237419 feat(api): introduce cache busting mechanism (#439) 2016-12-31 12:20:38 +13:00
Anthony Lapenna ecc8857a32 fix(global): strip leading '/' in front of endpoints (#438) 2016-12-31 10:30:22 +13:00
Anthony Lapenna 7d05e81c37 chore(github): update ISSUE_TEMPLATE.md 2016-12-27 08:54:39 +13:00
Anthony Lapenna 6ce3fe7a9e Merge tag '1.11.0' into develop
Release 1.11.0
2016-12-26 13:30:20 +13:00
Anthony Lapenna 9443284f52 Merge branch 'release/1.11.0' 2016-12-26 13:30:15 +13:00
Anthony Lapenna 4d6dadd17c chore(version): bump version number 2016-12-26 13:30:06 +13:00
Anthony Lapenna d54d30a7be feat(global): multi endpoint management (#407) 2016-12-26 09:34:02 +13:00
Glowbal a08ea134fc feat(container-creation): add ability to specify labels in the container creation view (#412) 2016-12-26 09:33:14 +13:00
Glowbal c9ba16ef10 feat(network-creation): add labels on network create (#408) 2016-12-26 09:32:17 +13:00
Glowbal 986171ecfe feat(service): Add editable service update configuration (#346)
* #304 Add editable service update configuration

* fix unable to use 0 for update-delay

* apply margin top to center help text
2016-12-26 09:31:22 +13:00
Glowbal 712b4528c0 feat(network-details): add list of containers in network (#302)
- shows all containers currently connected to the network
- abillity to disconect a container from the network
- fix error when a container is not connected to any networks
2016-12-26 09:28:54 +13:00
Anthony Lapenna 03456ddcf8 feat(containers): add the ability to filter by state (#410) 2016-12-25 22:43:53 +13:00
Anthony Lapenna ce32ed5b98 fix(service-creation): fix the command specification and add the ability to specify an entrypoint (#409) 2016-12-25 22:14:26 +13:00
Paul Kling edeed41797 #186 feat(container): bind the enter key when renaming container (#385) 2016-12-25 08:53:57 +13:00
David Eisner 419727e1eb feat(api): Connect to docker behind a name based virtual host proxy (#379)
This involves copying and modifying go's httputil.NewSingleHostReverseProxy, which is documented to (perhaps surprisingly) leave the Host header untouched. Instead, we set the Host header to the target host for the connection for the benefit of name based virtual host proxies that make use of this. The value it would otherwise have in this app, typically 'localhost:8000', is strange and unlikely to be any use.

See golang/go#7618 and golang/go#10342
2016-12-24 17:49:29 +13:00
Anthony Lapenna 9165b5b215 fix(dashboard): add missing dependency to Messages service (#402) 2016-12-21 11:24:34 +13:00
Anthony Lapenna 0a38bba874 refactor(api): API overhaul (#392) 2016-12-18 18:21:29 +13:00
Anthony Lapenna d9f6124609 refactor(global): remove useless code related to CSRF (#387) 2016-12-16 14:00:57 +13:00
Anthony Lapenna 5b16deb73e fix(templates): fix an issue with template creation introduced with #384 2016-12-16 13:39:24 +13:00
Anthony Lapenna 4e77c72fa2 feat(global): add authentication support with single admin account 2016-12-15 16:33:47 +13:00
Anthony Lapenna 1e5207517d fix(container-creation): do not stop container creation if unable to pull image 2016-12-15 14:30:35 +13:00
Anthony Lapenna 2a28921984 docs(README): update readthedocs badge to point at stable version 2016-12-14 09:46:01 +13:00
Anthony Lapenna b5bf7cdead feat(templates): add support for the template registry field 2016-12-14 09:33:24 +13:00
Paul Kling 8869a2c79c feat(templates): automatically scroll up to the app template form after selecting a template 2016-12-14 09:25:23 +13:00
Anthony Lapenna 99d49a1f87 chore(project): update contribution guidelines 2016-12-02 19:19:24 +13:00
Anthony Lapenna a53c0f08a3 Merge tag '1.10.2' into develop
Release 1.10.2
2016-11-26 00:51:01 +13:00
Anthony Lapenna 0e40bb13fc Merge branch 'release/1.10.2' 2016-11-26 00:50:55 +13:00
Anthony Lapenna db46087799 chore(version): bump version number 2016-11-26 00:50:50 +13:00
Anthony Lapenna 367a275672 fix(service-details): fix an issue with the '=' separator in env variable values (#370) 2016-11-25 20:48:12 +09:00
Glowbal b3a641e15a feat(service-creation): add support for container labels (#365) 2016-11-25 15:21:06 +09:00
Glowbal 868b400af3 fix(volumes): fix loading text displayed when no volumes present
Volumes is undefined when no volumes are present. The loading text will remain until volumes is defined.
2016-11-25 15:16:28 +09:00
Rob McFadzean 8fcae6810e fix(templates): fixes an issue regarding template selection when paged 2016-11-22 09:21:36 +09:00
Anthony Lapenna 913c580340 feat(UX): add pagination for all object lists (#352) 2016-11-17 21:50:46 +09:00
Anthony Lapenna 13a8b11d3d Merge tag '1.10.1' into develop
Release 1.10.1
2016-11-16 23:17:51 +13:00
Anthony Lapenna 5af99c6fe3 Merge branch 'release/1.10.1' 2016-11-16 23:17:46 +13:00
Anthony Lapenna 2d35ac8f82 chore(version): bump version number 2016-11-16 23:17:39 +13:00
Anthony Lapenna 3db487f386 fix(service-details): fix a sorting issue when ordering by last update (#350) 2016-11-16 19:16:50 +09:00
Rob Brazier 643769d4a6 feat(container-creation): add the ability to use container as a network 2016-11-16 10:52:05 +09:00
Anthony Lapenna 2c49d3b5d9 docs(README): add a donate badge 2016-11-12 12:51:06 +13:00
Anthony Lapenna 714f515f0b chore(build-system): fix build script 2016-11-11 15:50:59 +13:00
Anthony Lapenna 672479bf4f Merge tag '1.10.0' into develop
Release 1.10.0
2016-11-11 15:29:25 +13:00
Anthony Lapenna 8c3f7b3ec2 Merge branch 'release/1.10.0' 2016-11-11 15:29:16 +13:00
Anthony Lapenna 3aa0f4d263 chore(version): bump version number 2016-11-11 15:29:02 +13:00
Anthony Lapenna 2f35f04207 fix(service-details): fix an issue when trying to update a global service (#343) 2016-11-11 11:26:19 +09:00
Anthony Lapenna 3b3b23142c chore(build-system): add a release for macos task (#342) 2016-11-11 11:17:38 +09:00
Anthony Lapenna 9bd88fd10d style(service-details): fix wrong display for some fields (#340) 2016-11-10 13:01:03 +09:00
Glowbal 3092d0b7eb chore(grunt): adda run local swarm grunt task 2016-11-10 11:42:07 +09:00
Glowbal d924d340d7 feat(service-details): add the ability to edit the labels associated to a service 2016-11-10 11:38:49 +09:00
Glowbal c1ffd02491 fix(container-details): fix an issue with the leave network action 2016-11-10 11:25:31 +09:00
Glowbal 8e9dd8c2df #304 feat(service-details): add the ability to update a service env vars and image 2016-11-09 13:23:56 +13:00
Glowbal 1bfd6bbe95 #280 feat(service-creation): add labels to service creation (#306) 2016-11-07 17:57:33 +13:00
Glowbal 715638e368 feat(container-details): show list of joined networks (#303)
- Add overview of joined networks in container view
- Add option ot leave a joined network
2016-11-07 17:36:00 +13:00
jjlorenzo 08c868bc1c Restore the ability to customize the logo image. (#327) 2016-11-07 17:14:58 +13:00
Anthony Lapenna 9f46b12625 fix(containers): fix an issue with container IP in overlay network (#324) 2016-11-07 17:13:57 +13:00
Anthony Lapenna 6fc25691bd feat(backend): add a simple log message to indicate portainer startup (#320) 2016-11-04 16:52:02 +13:00
Anthony Lapenna c1713e0d01 docs(readme): update Portainer description with Windows support 2016-11-04 10:48:36 +13:00
Glowbal 8187f17d33 fix(service-details): show labels in service view 2016-11-03 17:14:07 +13:00
Anthony Lapenna f0e194f63b Disable CSRF protection (#313) 2016-11-03 15:56:10 +13:00
Glowbal eabf1f10e4 feat(navigation): add clickable url in breadcrumbs 2016-11-02 18:14:52 +13:00
Stefan Scherer c913d858ee Add Linux ARM support (#299)
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
2016-11-01 09:07:32 +13:00
Anthony Lapenna 17f35ef705 fix(container-creation): fix default network on Windows platform (#298) 2016-10-29 17:49:21 +13:00
Anthony Lapenna 0bdbb4a75d feat(container-stats): make process list sortable 2016-10-29 17:39:15 +13:00
Stefan Scherer f9327b3337 Use microsoft base images (#296)
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
2016-10-29 16:38:32 +13:00
Anthony Lapenna bf6c9c8b3b refactor(style): refactor multiple similar css classes 2016-10-27 21:33:39 +13:00
Anthony Lapenna 45015a573b feat(container-creation): add the unless stopped container restart policy (#294) 2016-10-27 20:05:37 +13:00
Anthony Lapenna d4f0145161 feat(templates): allow to edit template port mapping (#293)
* feat(templates): allow to edit template port mapping

* refactor(templates): remove advanced template configuration feature
2016-10-27 19:55:44 +13:00
Anthony Lapenna fa53339fea feat(docker): new docker view (#292) 2016-10-27 17:13:53 +13:00
Anthony Lapenna e5396091a7 feat(console): automatically determine command presets based on container image OS (#284) 2016-10-26 16:29:29 +13:00
Anthony Lapenna 1ae18e1577 chore(grunt): fix an issue with the Docker image building process in grunt 2016-10-26 12:09:09 +13:00
Anthony Lapenna b953850a1f chore(grunt): fix issue with grunt run-* tasks 2016-10-26 12:05:29 +13:00
Anthony Lapenna d0954abe29 chore(docker): update build system with Docker for Windows support (#283) 2016-10-26 09:04:26 +13:00
Anthony Lapenna c3cf5b5f9d feat(templates): advanced template creation (#277) 2016-10-20 16:43:09 +13:00
Anthony Lapenna 6589730acc refactor(css): remove useless css classes (#274) 2016-10-19 17:57:38 +13:00
Anthony Lapenna 442dcff0f1 chore(license): relicense to zlib license (#271) 2016-10-16 14:39:38 +13:00
Anthony Lapenna 8bac1955a8 Merge tag '1.9.3' into develop
Release 1.9.3
2016-10-09 10:50:52 +13:00
Anthony Lapenna 09a5534499 Merge branch 'release/1.9.3' 2016-10-09 10:50:46 +13:00
Anthony Lapenna 65c126f6a1 chore(version): bump version number 2016-10-09 10:50:32 +13:00
Anthony Lapenna 6adec680a4 style(templates): new effect on hover (#268)
* style(templates): new effect on hover

* feat(templates): display a loading message
2016-10-09 10:49:24 +13:00
Anthony Lapenna b81d4fa7f2 feat(global): display a loading text in list views (#267) 2016-10-08 14:59:58 +13:00
Anthony Lapenna d8f2e3da86 docs(readme): update README 2016-10-08 10:10:12 +13:00
Anthony Lapenna b0c0512515 Merge tag '1.9.2' into develop
Release 1.9.2
2016-10-07 18:22:58 +13:00
Anthony Lapenna bb9e044e89 Merge branch 'release/1.9.2' 2016-10-07 18:22:53 +13:00
Anthony Lapenna 520532cb9a chore(version): bump version number 2016-10-07 18:22:44 +13:00
Anthony Lapenna 44e09ecadf feat(container-creation): let Docker assign a port when host port is not specified (#264) 2016-10-07 18:08:07 +13:00
Anthony Lapenna 35ced4901a feat(global): display a message when no item available in a list view (#263) 2016-10-07 17:55:09 +13:00
Anthony Lapenna 134416c9a3 fix(container-console): use xterm.js v2 (#262) 2016-10-07 17:19:25 +13:00
Anthony Lapenna 8f7f4acc0d chore(build): add a build script to archive binary 2016-10-05 11:33:32 +13:00
Anthony Lapenna fde0d3ea9f chore(github): add github issue template 2016-10-05 10:56:49 +13:00
Anthony Lapenna 477799af7e chore(project): update contribution guidelines 2016-10-05 10:44:29 +13:00
Anthony Lapenna 72570153a5 docs(badges): add the dockerhub version badge 2016-10-03 12:35:40 +13:00
Anthony Lapenna 9f335b692f Merge tag '1.9.1' into develop
Release 1.9.1
2016-10-02 16:26:25 +13:00
Anthony Lapenna e88b22bd45 Merge branch 'release/1.9.1' 2016-10-02 16:26:19 +13:00
Anthony Lapenna 833053a2e1 chore(version): bump version number 2016-10-02 16:26:11 +13:00
Anthony Lapenna 64c52348f3 fix(lint): fix linting issue 2016-10-02 16:25:37 +13:00
Anthony Lapenna c3b79e6cc2 chore(xterm): update xterm.js version to 1.1.3 (#254) 2016-10-02 16:19:11 +13:00
Anthony Lapenna 422a982d60 feat(templates): template configuration is now placed on top of template list (#253) 2016-10-02 16:11:20 +13:00
Anthony Lapenna 6e9fe26fde fix(templates): fix bad container display when swarm-mode enabled (#251) 2016-10-02 15:05:40 +13:00
Anthony Lapenna 6bfa3096dc feat(index): hide Events and Docker view when swarm-mode is enabled (#250) 2016-10-02 14:57:01 +13:00
Anthony Lapenna 7cd2da4c6e fix(console): fix issue with undefined socket (#248) 2016-10-01 21:44:23 +13:00
Anthony Lapenna 739a5ec299 fix(general): fix the size display using the filesize library (#246)
* fix(general): fix the size display using the filesize library

* refactor(humansize): use default value for filter
2016-10-01 21:38:20 +13:00
Anthony Lapenna 59e65222eb feat(swarm): support Swarm replica management (#245) 2016-10-01 17:50:46 +13:00
Anthony Lapenna 01d5d11c01 feat(events): support new events (#244) 2016-10-01 17:08:32 +13:00
Anthony Lapenna 29a59cab44 feat(containers): change exposed port format (#243) 2016-10-01 16:55:11 +13:00
Anthony Lapenna be184c11a6 style(favicon): update favicon (#242) 2016-10-01 16:51:45 +13:00
Anthony Lapenna d6ab97ad25 fix(containers): fix the ability to sort containers by status (#241) 2016-10-01 16:45:06 +13:00
Anthony Lapenna 6a0f76890e docs(README): update README 2016-09-30 18:51:55 +13:00
Anthony Lapenna 1946868248 docs(README): update links to readthedocs 2016-09-30 18:51:09 +13:00
Anthony Lapenna 84b02c711a docs(badge): add readthedocs badge 2016-09-30 18:47:36 +13:00
Anthony Lapenna 679a681749 Merge tag '1.9.0' into develop
Release 1.9.0
2016-09-24 22:33:37 +12:00
Anthony Lapenna c35d1b14ec Merge branch 'release/1.9.0' 2016-09-24 22:33:30 +12:00
Anthony Lapenna 87df297a56 chore(version): bump version number 2016-09-24 22:33:23 +12:00
Anthony Lapenna b8e420e0e8 docs(project): new documentation (#229) 2016-09-24 22:31:37 +12:00
Anthony Lapenna f8c8668863 docs(contribution): add contributions rules 2016-09-24 17:30:08 +12:00
Anthony Lapenna ced0746a81 Merge pull request #228 from portainer/chore218-portainer-org
chore(global): replace CloudInovasi with Portainer.io
2016-09-23 18:29:09 +12:00
Anthony Lapenna 39909d774f chore(global): replace CloudInovasi with Portainer.io 2016-09-23 18:28:20 +12:00
Anthony Lapenna 12e6e0557d Merge pull request #227 from cloud-inovasi/feat216-swarm-latest-support
feat(global): change the strategy used to determine if swarm mode is …
2016-09-23 18:02:48 +12:00
Anthony Lapenna e27282de3c feat(global): change the strategy used to determine if swarm mode is used 2016-09-23 18:02:03 +12:00
Anthony Lapenna fe63f9939a Merge pull request #226 from cloud-inovasi/style219-incoherent-container-icon
style(ui): use fa-server icon instead of fa-tasks for container entity
2016-09-23 17:20:52 +12:00
Anthony Lapenna b623a5d452 style(ui): use fa-server icon instead of fa-tasks for container entity 2016-09-23 17:19:57 +12:00
Anthony Lapenna d8113df979 Merge pull request #225 from cloud-inovasi/style220-github-icon
style(index): add a github icon next to the github link
2016-09-23 17:08:17 +12:00
Anthony Lapenna b3ba36c02a style(index): add a github icon next to the github link 2016-09-23 17:07:48 +12:00
Anthony Lapenna 37863e3f74 feat(global): swarm mode support (#213)
feat(global): swarm mode support
2016-09-23 16:54:58 +12:00
Anthony Lapenna da6f39b137 fix(lint): fix jshint issue 2016-09-14 17:50:54 +12:00
Anthony Lapenna 4fe63d7102 Merge pull request #211 from cloud-inovasi/feat200-network-view
feat(network): new network view
2016-09-14 17:49:24 +12:00
Anthony Lapenna 7c8881f37d feat(network): new network view 2016-09-14 17:48:20 +12:00
Anthony Lapenna c20069fce0 Merge pull request #210 from cloud-inovasi/feat195-quick-network-creation-form
feat(networks): add a quick network creation form
2016-09-14 16:31:02 +12:00
Anthony Lapenna 2eb1c9e857 feat(networks): add a quick network creation form 2016-09-14 16:28:38 +12:00
Anthony Lapenna 48e1fe769e Merge pull request #209 from cloud-inovasi/style199-action-icons
style(actions): add icons for every actions
2016-09-14 15:32:31 +12:00
Anthony Lapenna 2b8bc82d4e style(actions): add icons for every actions 2016-09-14 15:30:52 +12:00
Anthony Lapenna 8f33151647 Merge tag '1.8.1' into develop
Release 1.8.1
2016-09-07 18:31:48 +12:00
Anthony Lapenna 8e743a8d32 Merge branch 'release/1.8.1' 2016-09-07 18:31:44 +12:00
Anthony Lapenna 9f22e01d3b chore(version): bump version number 2016-09-07 18:31:32 +12:00
Anthony Lapenna 502c8718c5 Merge pull request #206 from cloud-inovasi/feat196-disable-create-button
feat(network-creation): disable create button while network name is e…
2016-09-07 18:28:57 +12:00
Anthony Lapenna 220faa52e7 feat(network-creation): disable create button while network name is empty 2016-09-07 18:28:14 +12:00
Anthony Lapenna 857c93bff9 Merge pull request #205 from cloud-inovasi/fix185-volume-deletion-error
fix(volumes): display an error message when trying to delete a bound …
2016-09-07 18:23:11 +12:00
Anthony Lapenna ca5cf33c8f fix(volumes): display an error message when trying to delete a bound volume 2016-09-07 18:21:46 +12:00
Anthony Lapenna 1cd620a45e Merge pull request #204 from cloud-inovasi/fix193-image-error-message
fix(image): support array in Messages.error
2016-09-07 18:05:15 +12:00
Anthony Lapenna 4eb9a9a0af fix(image): support array in Messages.error 2016-09-07 18:03:55 +12:00
Anthony Lapenna c82abae8e5 Merge pull request #203 from cloud-inovasi/bug198-hidden-containers
fix(containers): make hidden containers labels available in the $scope
2016-09-07 16:42:45 +12:00
Anthony Lapenna f56256f897 fix(containers): make hidden containers labels available in the $scope 2016-09-07 16:38:54 +12:00
Anthony Lapenna e31749e64d Merge pull request #202 from cloud-inovasi/style201-latest-logo
style(logo): use latest logo
2016-09-07 16:25:57 +12:00
Anthony Lapenna 89d666f365 style(logo): use latest logo 2016-09-07 16:25:21 +12:00
Anthony Lapenna b502852966 chore(badge): add the microbadger badge 2016-09-05 09:19:34 +12:00
Anthony Lapenna e101397a2c chore(gitter): add gitter badge 2016-09-04 15:33:59 +12:00
Anthony Lapenna ddcecc06d4 Merge tag '1.8.0' into develop
Release 1.8.0
2016-09-04 15:11:08 +12:00
Anthony Lapenna 4237f452df Merge branch 'release/1.8.0' 2016-09-04 15:11:03 +12:00
Anthony Lapenna 3f9276ee4c chore(version): bump version number 2016-09-04 15:10:47 +12:00
Anthony Lapenna 5a1f437cf9 fix(lint): fix linting issue 2016-09-04 15:06:33 +12:00
Anthony Lapenna bb9cebd759 Merge pull request #191 from cloud-inovasi/refactor153-rename-to-portainer
Refactor153 rename to portainer
2016-09-04 15:01:02 +12:00
Anthony Lapenna 62e313d13f style(docs): update dashboard picture 2016-09-04 14:53:01 +12:00
Anthony Lapenna 537ee24078 refactor(global): rename uifd to portainer 2016-09-04 14:50:37 +12:00
Anthony Lapenna 364756d9fa Merge pull request #190 from cloud-inovasi/docs95-docker112-support
docs(version): update Docker versions support section
2016-09-04 12:06:12 +12:00
Anthony Lapenna 6eb1cff8c5 docs(version): update Docker versions support section 2016-09-04 12:04:42 +12:00
Anthony Lapenna 44e02c0342 fix(network): add missing exception management 2016-09-02 17:59:32 +12:00
Anthony Lapenna b36767cdb7 Merge pull request #189 from cloud-inovasi/feat95-exception-mgmt
feat(ui): add missing exception management
2016-09-02 17:52:34 +12:00
Anthony Lapenna 67194109c6 feat(ui): add missing exception management 2016-09-02 17:51:41 +12:00
Anthony Lapenna 08032be2c4 Merge pull request #188 from cloud-inovasi/feat95-exception-mgmt
feat(ui): new exception management
2016-09-02 17:41:10 +12:00
Anthony Lapenna 74b97a0036 feat(ui): new exception management 2016-09-02 17:40:03 +12:00
Anthony Lapenna eac3239817 Merge pull request #187 from cloud-inovasi/feat95-remove-errormsgfilter
feat(ui): remove the errorMsg filter and replace it with proper error…
2016-09-02 15:26:26 +12:00
Anthony Lapenna 9698aa7ad5 feat(ui): remove the errorMsg filter and replace it with proper error management 2016-09-02 15:25:20 +12:00
Anthony Lapenna cbce2a70f5 Merge pull request #184 from cloud-inovasi/feat95-container-start-no-hostconfig
feat(container): do not pass HostConfig when starting a container
2016-09-02 13:52:32 +12:00
Anthony Lapenna a2d91ec2f9 feat(container): do not pass HostConfig when starting a container 2016-09-02 13:51:49 +12:00
Anthony Lapenna d93a69df95 Merge pull request #181 from cloud-inovasi/feat95-responsehandler-generic-handler
feat(container): add a deletion generic handler used for container/ne…
2016-09-01 15:08:12 +12:00
Anthony Lapenna fb982ca8f1 feat(container): add a deletion generic handler used for container/network deletion 2016-09-01 15:07:31 +12:00
Anthony Lapenna 4b979628b3 Merge pull request #180 from cloud-inovasi/feat95-responsehandler-image-delete
feat(image): define a new response handler for image deletion
2016-09-01 14:26:49 +12:00
Anthony Lapenna 789750cc86 feat(image): define a new response handler for image deletion 2016-09-01 14:24:47 +12:00
Anthony Lapenna 4125361fb5 Merge pull request #179 from cloud-inovasi/feat-responsehandler-delete-network
feat(network): define a response handler for image deletion
2016-09-01 12:21:44 +12:00
Anthony Lapenna 6b8b562e7c feat(network): define a response handler for image deletion 2016-09-01 12:20:19 +12:00
Anthony Lapenna 2e9a117255 Merge pull request #178 from cloud-inovasi/feat176-network-error-message
feat(network): display the correct error message when a network delet…
2016-09-01 11:33:15 +12:00
Anthony Lapenna 6d6a7e6923 feat(network): display the correct error message when a network deletion failure occurs 2016-09-01 11:31:25 +12:00
Anthony Lapenna 4edb4e014f refactor(ui): introduce helpers functions to centralize code (#174) 2016-08-31 18:06:10 +12:00
Anthony Lapenna f020e5a633 refactor(ui): introduce helpers functions to centralize code 2016-08-31 18:03:41 +12:00
Anthony Lapenna 5432424a40 fix(image): fix the deleteImageHandler so that messages are correctly displayed in the UI (#172) 2016-08-31 11:26:02 +12:00
Anthony Lapenna e81bfb6f37 fix(templates): hide hidden containers in templates (#165) 2016-08-24 20:26:51 +12:00
Anthony Lapenna 3c75c5fe25 refactor(templates): use the set field instead of default (#164) 2016-08-24 19:46:31 +12:00
Anthony Lapenna 7c5c693f17 feat(templates): support select for env fields with type 'container' (#163) 2016-08-24 18:32:54 +12:00
Anthony Lapenna 2d98e33e98 style(template): Update title and section name (#162) 2016-08-24 15:45:44 +12:00
Anthony Lapenna 4827d33ca1 feat(templates): support env variables with default value (#161) 2016-08-24 15:30:29 +12:00
Anthony Lapenna 71eb3feac9 feat(containers): update the containers view to add a column with exposed ports (#157) 2016-08-24 10:58:55 +12:00
Anthony Lapenna 5f290937d2 refactor(templates): rename field comment to description (#155) 2016-08-23 18:49:40 +12:00
Anthony Lapenna 1c8aa35479 feat(global): add templates support ('apps') (#154) 2016-08-23 18:09:14 +12:00
Anthony Lapenna faccf2a651 feat(container): container view overhaul (#150) 2016-08-19 18:41:45 +12:00
Anthony Lapenna 4d99c12215 fix(image): display a valid error message when deleting an image (#149)
fix(image): display a valid error message when deleting an image
2016-08-19 17:53:27 +12:00
Anthony Lapenna 7c2047cfbf feat(containers): rename the column header Host to Host IP (#145) 2016-08-18 16:55:19 +12:00
Anthony Lapenna 12d5cfe8e4 Merge tag '1.7.0' into develop
Release 1.7.0
2016-08-18 15:47:45 +12:00
Anthony Lapenna cbbcb51162 Merge branch 'release/1.7.0' 2016-08-18 15:47:37 +12:00
Anthony Lapenna 954a6a11b7 chore(version): bump version number 2016-08-18 15:47:18 +12:00
Anthony Lapenna 0c5e98b47d Updated README. 2016-08-17 21:55:30 +12:00
Anthony Lapenna d941fef8d6 docs(logo): add documentation about the --logo flag (#141) 2016-08-17 21:52:49 +12:00
Anthony Lapenna 496de850c1 fix(container-creation): allow to specify an address in the host port binding (#139) 2016-08-17 19:31:06 +12:00
Anthony Lapenna 29fa33fb2b fix(container-creation): fix unregistered ports bindings when creating a container (#138) 2016-08-17 19:03:36 +12:00
Anthony Lapenna 06fbb5ba34 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 19:01:28 +12:00
Anthony Lapenna a32f6f343d refactor(container-creation): remove changes related to internal version (#137) 2016-08-17 18:43:53 +12:00
Anthony Lapenna 61d7b4f64c refactor(container-creation): remove changes related to internal version 2016-08-17 18:42:56 +12:00
Anthony Lapenna 1840ab4bba docs(reverse-proxy): add reverse proxy instructions (#136) 2016-08-17 18:31:23 +12:00
Anthony Lapenna ccb812cc33 feat(console): add protocol awareness to switch between ws:// and wss:// (#135) 2016-08-17 18:23:30 +12:00
Anthony Lapenna b098cd5638 feat(image): add the ability to tag an image (#134) 2016-08-17 18:05:17 +12:00
Anthony Lapenna eefa7ca138 refactor(global): revert merge with internal (#133) 2016-08-17 17:25:42 +12:00
Anthony Lapenna b5dcdc8807 refactor(api): remove the binary from versioning (#128) 2016-08-17 13:57:51 +12:00
Anthony Lapenna 4b4e5d5ebd Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 13:52:52 +12:00
Anthony Lapenna 54fd9561f0 refactor(handlers): remove duplicated code (#127) 2016-08-17 13:50:55 +12:00
Anthony Lapenna fb67769928 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 13:49:17 +12:00
Anthony Lapenna 9c8e632a09 merge branch 'feat107-push-registry' into internal 2016-08-17 12:32:09 +12:00
Anthony Lapenna 0b6c2b032a feat(image): add the ability to push an image tag (#126) 2016-08-17 12:29:13 +12:00
Anthony Lapenna f0e4cdc13e feat(image): add the ability to push an image tag 2016-08-17 12:27:54 +12:00
Anthony Lapenna cfe31fbeac merge branch feat106-external-logo into internal 2016-08-10 18:40:05 +12:00
Anthony Lapenna 722dc0b3af feat(global): add the --logo flag to specify an external logo picture (#120) 2016-08-10 18:38:33 +12:00
Anthony Lapenna de3353feba feat(global): add the --logo flag to specify an external logo picture 2016-08-10 18:37:25 +12:00
Anthony Lapenna 145e45b4a8 feat(ui): network creation support for standalone engine (#119) 2016-08-10 18:20:18 +12:00
Anthony Lapenna d0f57809d6 merge branch 'feat57-swarm-host-ip' into internal 2016-08-10 18:13:39 +12:00
Anthony Lapenna 6c29377992 feat(ui): display swarm host IP instead of swarm hostname in containers view (#118) 2016-08-10 18:12:33 +12:00
Anthony Lapenna 164902c0cb feat(ui): display swarm host IP instead of swarm hostname in containers view 2016-08-10 18:11:36 +12:00
Anthony Lapenna 75466cb57f merge branch 'feat96-date-format-iso8601' into internal 2016-08-10 16:06:35 +12:00
Anthony Lapenna 0e8fff7a51 feat(ui): format all dates to use ISO8601 (#117) 2016-08-10 16:05:28 +12:00
Anthony Lapenna 7b72da857f feat(ui): format all dates to use ISO8601 2016-08-10 16:04:19 +12:00
Anthony Lapenna b89546a1e0 Merge branch 'fix-108-dashboard-no-volumes' into internal 2016-08-10 15:41:34 +12:00
Anthony Lapenna 22122a27b5 fix(dashboard): fix an error when no volumes are availables (#116) 2016-08-10 15:40:35 +12:00
Anthony Lapenna 24a9e9f61c fix(dashboard): fix an error when no volumes are availables 2016-08-10 15:39:59 +12:00
Anthony Lapenna cf5378f604 Merge branch 'chore-revert-libraries' into internal 2016-08-10 15:33:15 +12:00
Anthony Lapenna 1d8f51c141 chore(gruntfile): revert commit to use minified libraries (#115) 2016-08-10 15:32:39 +12:00
Anthony Lapenna 87798cd1c8 chore(gruntfile): revert commit to use minified libraries 2016-08-10 15:31:58 +12:00
Anthony Lapenna fd1496df93 Merge branch 'fix-113-images-view-delete' into internal 2016-08-10 15:27:32 +12:00
Anthony Lapenna ea6e11000d fix(images): display an error message when unable to remove an image (#114) 2016-08-10 15:27:03 +12:00
Anthony Lapenna ef257f65cf fix(images): display an error message when unable to remove an image 2016-08-10 15:26:08 +12:00
Anthony Lapenna 01a707c8e7 merge branch 'refactor-image-view' into internal 2016-08-10 15:15:54 +12:00
Anthony Lapenna 9293b28ef4 refactor(ui): updated image view (#112) 2016-08-10 15:14:10 +12:00
Anthony Lapenna 30c0fda1b6 refactor(ui): updated image view 2016-08-10 15:05:33 +12:00
Anthony Lapenna 548a458b9a Merge branch "feat107-remove-select-all" into internal 2016-08-04 11:31:35 +12:00
Anthony Lapenna 111cd4ac64 feat(ui): remove the ability to select all entities from list views (#104) 2016-08-04 11:29:29 +12:00
Anthony Lapenna 54ab81a7de feat(ui): remove the ability to select all entities from list views 2016-08-04 11:17:34 +12:00
Anthony Lapenna 21344280a9 Merge tag '1.6.0' into develop
Release 1.6.0
2016-08-03 21:56:14 +12:00
Anthony Lapenna d3fa9736f4 Merge branch 'release/1.6.0' 2016-08-03 21:56:09 +12:00
Anthony Lapenna f1ec419e3a chore(version): bump version number 2016-08-03 21:50:01 +12:00
Anthony Lapenna de8c6b4ed8 Merge branch 'style-dashboard-widget-icon' into internal 2016-08-03 21:46:33 +12:00
Anthony Lapenna e661cef2fe style(dashboard): change the icon in the main widget (#102) 2016-08-03 21:46:08 +12:00
Anthony Lapenna edf485bbe4 style(dashboard): change the icon in the main widget 2016-08-03 21:45:35 +12:00
Anthony Lapenna 20eecffc40 Merge branch "feat99-container-exec-event" into internal 2016-08-03 21:31:32 +12:00
Anthony Lapenna 232b180eef feat(events): add support for container exec related events (#100) 2016-08-03 21:28:27 +12:00
Anthony Lapenna 4a738ee362 feat(events): add support for container exec related events 2016-08-03 21:27:51 +12:00
Anthony Lapenna f4d90306b3 Merge branch "refactor-backend-settings" into internal 2016-08-03 21:18:46 +12:00
Anthony Lapenna 7801a91149 refactor(global): replace /config endpoint with /settings to avoid confusion (#98) 2016-08-03 21:13:17 +12:00
Anthony Lapenna 19d4e38d94 refactor(global): replace /config endpoint with /settings to avoid confusion 2016-08-03 21:12:46 +12:00
Anthony Lapenna bab57e0402 Merge branch 'feat34-container-exec' into internal 2016-08-03 15:28:51 +12:00
Anthony Lapenna d2b3360bff Merge branch 'refactor-backend' into internal 2016-08-03 15:19:22 +12:00
Anthony Lapenna 1aaa5acbef feat(global): add container exec support (#97) 2016-08-03 15:12:53 +12:00
Anthony Lapenna 5878eed7ec feat(global): add container exec support 2016-08-03 15:11:09 +12:00
Anthony Lapenna b0ebbdf68c refactor(api): create a new structure for the Go api (#94)
* refactor(api): create a new structure for the Go api

* refactor(api): update the way keyFile parameter is managed
2016-08-01 13:40:12 +12:00
Anthony Lapenna 0ec20d3093 refactor(api): update the way keyFile parameter is managed 2016-07-31 17:46:05 +12:00
Anthony Lapenna c5ddae12cf refactor(api): create a new structure for the Go api 2016-07-29 15:58:11 +12:00
Anthony Lapenna b1e1850e9f Merge branch 'feat91-swarm-node-info' into internal 2016-07-27 20:00:33 +12:00
Anthony Lapenna 06c2635e82 feat(ui): add more info about nodes in Swarm view (#92)
* feat(ui): add more info about nodes in Swarm view

* style(ui): update title for section in swarm view
2016-07-27 20:00:00 +12:00
Anthony Lapenna 711ac742e1 style(ui): update title for section in swarm view 2016-07-27 19:59:41 +12:00
Anthony Lapenna 201ab20131 feat(ui): add more info about nodes in Swarm view 2016-07-27 19:54:31 +12:00
Anthony Lapenna 716ba72217 Merge branch 'feat52-active-network-remove-error' into internal 2016-07-27 17:38:10 +12:00
Anthony Lapenna 95b16919a6 feat(ui): display an error message when trying to remove a network with active endpoints (#90) 2016-07-27 17:37:35 +12:00
Anthony Lapenna d3d000a1d0 feat(ui): display an error message when trying to remove a network with active endpoints 2016-07-27 17:36:22 +12:00
Anthony Lapenna 9499f78121 Merge branch 'docs82-image-preview' into internal 2016-07-27 17:17:22 +12:00
Anthony Lapenna fa36c9ee5c docs(README): update dashboard.png (#89) 2016-07-27 17:17:00 +12:00
Anthony Lapenna c460eb4d7a docs(README): update dashboard.png 2016-07-27 17:16:25 +12:00
Anthony Lapenna ab52270238 merge branch refactor80-module-names into internal 2016-07-27 17:13:08 +12:00
Anthony Lapenna 7c6fdebb3d refactor(ui): rename angular modules from dockerui to uifordocker (#88) 2016-07-27 17:11:24 +12:00
Anthony Lapenna cf3cd76064 refactor(ui): rename angular modules from dockerui to uifordocker 2016-07-27 17:10:25 +12:00
Anthony Lapenna 5ef6b536ac Merge branch 'feat71-events-view' into internal 2016-07-27 17:06:04 +12:00
Anthony Lapenna adf5184a5d feat(ui): add events view (#86)
* feat(ui): add events view

* chore(grunt): use minified angular script
2016-07-27 17:05:16 +12:00
Anthony Lapenna ea596a8701 fix(ui): config endpoint is available at config rather than /config (#83) 2016-07-27 17:04:47 +12:00
Anthony Lapenna 15a3cb7241 chore(grunt): use minified angular script 2016-07-27 11:10:42 +12:00
Anthony Lapenna f147da3017 feat(ui): add events view 2016-07-27 11:08:18 +12:00
Anthony Lapenna dfaf2eb6a9 merge branch nginx-support into internal 2016-07-21 11:15:32 +12:00
Anthony Lapenna 97f6a32c78 fix(ui): config endpoint is available at config rather than /config 2016-07-21 11:14:29 +12:00
Anthony Lapenna bcdd7498a1 chore(version): bump version number 2016-07-14 12:18:29 +12:00
Anthony Lapenna c45947b573 Merge tag '1.5.0' into develop
Release 1.5.0
2016-07-14 12:12:18 +12:00
Anthony Lapenna 85140c7dcf Merge branch 'release/1.5.0' 2016-07-14 12:12:13 +12:00
Anthony Lapenna 30e9a604cd chore(version): bump version number 2016-07-14 12:12:01 +12:00
Anthony Lapenna 8c769148ad refactor(ui): remove console logging 2016-07-14 12:07:37 +12:00
Anthony Lapenna 4cc08d7211 refactor(ui): remove console logging 2016-07-14 12:07:07 +12:00
Anthony Lapenna 48b6b6340b Merge branch 'feat68-update-repository-field' into internal 2016-07-14 12:03:57 +12:00
Anthony Lapenna b857970236 feat(ui): replace repository field with tags field in image view (#79) 2016-07-14 12:02:42 +12:00
Anthony Lapenna 1011fde9de feat(ui): replace repository field with tags field in image view 2016-07-14 12:01:57 +12:00
Anthony Lapenna bee89720d5 refactor(ui): fix lint issue 2016-07-14 11:31:58 +12:00
Anthony Lapenna 23bff41304 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-14 11:31:10 +12:00
Anthony Lapenna 8243326692 refactor(ui): fix lint issue 2016-07-14 11:29:41 +12:00
Anthony Lapenna 17ae122595 Merge branch 'style73-widget-header-extra-space' into internal 2016-07-14 11:25:01 +12:00
Anthony Lapenna b69d72fc8c Merge branch 'style72-table-style' into internal 2016-07-14 11:24:54 +12:00
Anthony Lapenna c52498993b Merge branch 'style70-typo-engine-view' into internal 2016-07-14 11:24:45 +12:00
Anthony Lapenna a4a82b4502 merge branch feat54-revamp-internal into internal 2016-07-14 11:24:20 +12:00
Anthony Lapenna 52d953a1c2 style(ui): fix typo in Engine view (#76) 2016-07-14 11:16:23 +12:00
Anthony Lapenna e145d82947 style(ui): fix extra space in widget-header (#78) 2016-07-14 11:16:16 +12:00
Anthony Lapenna 25df1fe26c style(ui): add table-hover class to all entity tables (#77) 2016-07-14 11:16:10 +12:00
Anthony Lapenna 106718f416 style(ui): fix extra space in widget-header 2016-07-14 11:10:38 +12:00
Anthony Lapenna 8464faa2a1 style(ui): add table-hover class to all entity tables 2016-07-14 11:03:40 +12:00
Anthony Lapenna 9354b911bb style(ui): fix typo in Engine view 2016-07-14 11:00:26 +12:00
Anthony Lapenna c8a5b82c89 feat(ui): new dashboard view (#75) 2016-07-14 10:58:39 +12:00
Anthony Lapenna 1f884e9584 chore(version): bumped version number 2016-07-13 15:06:10 +12:00
Anthony Lapenna 00b2c92e39 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-13 14:54:35 +12:00
Anthony Lapenna 0796778d17 Merge tag '1.4.0' into develop
Release 1.4.0
2016-07-13 14:54:07 +12:00
Anthony Lapenna 1eae1c03f0 Merge branch 'release/1.4.0' 2016-07-13 14:54:03 +12:00
Anthony Lapenna a9209da167 chore(version): bump version number 2016-07-13 14:53:24 +12:00
Anthony Lapenna 43c2f14289 docs(docker): add info about Docker version support (#64) 2016-07-13 14:47:24 +12:00
Anthony Lapenna f378d56543 fix(ui): fix bad name for image field in container creation view 2016-07-13 13:47:15 +12:00
Anthony Lapenna 521d146d7b refactor(ui): remove useless createNetwork sources 2016-07-13 13:46:06 +12:00
Anthony Lapenna dc721f5870 merge feat66-docker-cli-compliant 2016-07-13 13:43:58 +12:00
Anthony Lapenna 3b0d726c2a feat(dockerui): Docker CLI compliant flags (#67) 2016-07-13 12:44:31 +12:00
Anthony Lapenna f6226d19b8 feat(dockerui): Docker CLI compliant flags 2016-07-13 12:42:20 +12:00
Anthony Lapenna 71c091ae0d feat(ui): docker 1.9 support (#65) 2016-07-13 10:53:03 +12:00
Anthony Lapenna 1fb008212a feat(dockerui): add support for TLS enabled engines (#63) 2016-07-12 20:31:11 +12:00
Anthony Lapenna cab34e4069 feat(network): add advanced settings in network creation (subnet/gateway) 2016-07-08 17:26:18 +12:00
Anthony Lapenna e67e20ce18 feat(network): add the ability to specify a subnet/gateway when creating a network (#53) 2016-07-08 17:12:33 +12:00
Anthony Lapenna d253c0d494 chore(version): bump version number 2016-07-08 16:26:29 +12:00
Anthony Lapenna c74e8fc732 style(lint): fix jshint issue 2016-07-08 16:20:31 +12:00
Anthony Lapenna 29358e5744 Merge tag '1.3.0' into develop
Release 1.3.0
2016-07-08 16:06:58 +12:00
Anthony Lapenna b59c102098 Merge branch 'release/1.3.0' 2016-07-08 16:06:53 +12:00
Anthony Lapenna afaa1433ff chore(version): bump version number 2016-07-08 16:06:46 +12:00
Anthony Lapenna f923016052 style(lint): fix jshint issues 2016-07-08 15:50:16 +12:00
Anthony Lapenna ca27e7f27a fix(containerCreation): fix an issue when creating an image from a custom registry without automatic pulling (#50) 2016-07-08 15:40:13 +12:00
Anthony Lapenna 8fd9c2fce2 refactor(ui): remove useless logging statement 2016-07-08 15:39:32 +12:00
Anthony Lapenna d4ca060945 feat(ui): add the ability to pull an image from a selection of registry 2016-07-08 15:31:09 +12:00
Anthony Lapenna d124c21d1b feat(ui): add the ability to create a container from an image in a custom registry (#49) 2016-07-08 12:52:26 +12:00
Anthony Lapenna d2fb2cb863 feat(ui): add the ability to pull an image from a private registry (#47) 2016-07-08 11:57:24 +12:00
Anthony Lapenna 0350daca8d Merge branch 'hide-containers-dashboard-swarm' into internal 2016-07-07 15:38:42 +12:00
Anthony Lapenna 06f54e300c fix(ui): hidden containers (using label) are now removed from dashboard and swarm view (#46) 2016-07-07 15:37:09 +12:00
Anthony Lapenna 135b940897 refactor(grunt): remove testing option 2016-07-07 15:35:45 +12:00
Anthony Lapenna 7856276092 fix(ui): hidden containers (using label) are now removed from dashboard and swarm view 2016-07-07 15:34:05 +12:00
Anthony Lapenna bf14dcc3e8 merge display-all-containers 2016-07-07 14:39:26 +12:00
Anthony Lapenna 21c1778822 feat(ui): default to display all containers (#45) 2016-07-07 14:31:16 +12:00
Anthony Lapenna 337bfa74bb feat(ui): default to display all containers 2016-07-07 14:30:11 +12:00
Anthony Lapenna 418b1ff544 fix(ui): fix display issue with multiple nodes in Swarm view 2016-07-07 13:25:22 +12:00
Anthony Lapenna 092d866c73 fix(ui): fix display issue with multiple nodes in Swarm view (#44) 2016-07-07 13:22:31 +12:00
Anthony Lapenna 50391c87e2 feat(ui): replace ViewSpinner with JQuery animations (#43) 2016-07-07 13:17:44 +12:00
Anthony Lapenna fd6645d068 merge fix-viewspinner 2016-07-07 13:15:56 +12:00
Anthony Lapenna 3a6e326e5e feat(ui): replace ViewSpinner with JQuery animations 2016-07-07 12:44:58 +12:00
Anthony Lapenna b997b787c4 feat(ui): simplify views for internal usage 2016-07-06 19:04:45 +12:00
Anthony Lapenna d227bdfc75 refactor(ui): remove useless controller declarations 2016-07-06 17:42:56 +12:00
Anthony Lapenna 4ba6286c97 Merge tag '1.2.0' into develop
Release 1.2.0
2016-07-06 16:41:33 +12:00
Anthony Lapenna 56ef453203 Merge branch 'release/1.2.0' 2016-07-06 16:41:28 +12:00
Anthony Lapenna b573a8bafa chore(version): bump version number 2016-07-06 16:41:21 +12:00
Anthony Lapenna 59820e737e feat(ui): new pull image view (#39) 2016-07-06 16:32:46 +12:00
Anthony Lapenna 530eb20dfc feat(ui): new network creation view (#37) 2016-07-06 15:38:34 +12:00
Anthony Lapenna 446322dcbe feat(ui): new volume creation view (#36) 2016-07-06 15:14:40 +12:00
Anthony Lapenna 2d311518a7 refactor(ui): fix jshint issue 2016-07-06 14:21:00 +12:00
Anthony Lapenna 3bcd1bf665 chore(grunt): add new lint task 2016-07-06 14:20:29 +12:00
Anthony Lapenna 88d5e22532 Merge tag '1.1.0' into develop
Release 1.1.0
2016-07-06 14:04:46 +12:00
Anthony Lapenna 41a41cdf38 Merge branch 'release/1.1.0' 2016-07-06 14:04:41 +12:00
Anthony Lapenna e6e21e9f46 chore(version): bump version number 2016-07-06 14:04:31 +12:00
Anthony Lapenna f18aa8fe79 fix(ui): fix display of containers per node in Swarm view (#30) 2016-07-06 12:24:49 +12:00
Anthony Lapenna 227e5883e9 feat(ui): new container creation view (#29) 2016-07-06 12:19:09 +12:00
Anthony Lapenna 87e835e873 feat(ui): display an error message when trying to remove a running container (#28) 2016-06-29 22:11:22 +12:00
Anthony Lapenna 965a099495 fix(flags): fix grunt run-swarm command and update long flag format (#26) 2016-06-29 21:08:36 +12:00
Anthony Lapenna 66ae15b4fb feat(ui): new containers view (#25)
feat(ui): new containers view
2016-06-29 21:04:29 +12:00
Anthony Lapenna 813c14d93c feat(ui): automatically pull the image when creating a container (#24)
feat(ui): automatically pull the image when creating a container
2016-06-29 18:09:50 +12:00
Anthony Lapenna 5d0af27a3f fix(binary): persist CSRF auth file in a volume (#22)
* fix(binary): persist CSRF auth file in a volume

* docs(options): document the `-data` option
2016-06-29 18:08:50 +12:00
Anthony Lapenna aa3fda6de9 Merge tag '1.0.4' into develop
Release 1.0.4
2016-06-24 10:24:45 +12:00
Anthony Lapenna 9e4f8c9fee Merge branch 'release/1.0.4' 2016-06-24 10:24:38 +12:00
Anthony Lapenna ce2e6f80fc chore(version): bump version number 2016-06-24 10:24:27 +12:00
Anthony Lapenna bf4622e4f5 refactor(ui): remove useless logging 2016-06-24 10:23:12 +12:00
Anthony Lapenna 9655f57698 docs(global): review documentation 2016-06-24 10:22:04 +12:00
Anthony Lapenna 808694d6b5 feat(global): hide containers with labels using -l flag (#19) 2016-06-24 10:11:49 +12:00
Anthony Lapenna cd12243b0f feat(ui): latest Swarm API support (#18)
* feat(ui): latest Swarm API support
2016-06-24 10:11:25 +12:00
Anthony Lapenna abfa921b7a feat(ui): new logo size 2016-06-23 17:36:01 +12:00
Anthony Lapenna 91f3b1f138 refactor(service): Config factory returns a promise 2016-06-21 18:35:21 +12:00
Anthony Lapenna 9468839bf9 chore(grunt): run-swarm task expect a Swarm cluster at 10.0.7.10:4000 2016-06-21 18:34:32 +12:00
Anthony Lapenna 9ca2aa9bbd refactor(dockerui): replace -s flag with -swarm 2016-06-21 12:27:32 +12:00
Anthony Lapenna 54c82a3a5c add section in README about Swarm support 2016-06-16 17:39:59 +12:00
Anthony Lapenna 9360693f8d clean:all on release grunt task 2016-06-16 17:37:57 +12:00
Anthony Lapenna c54dd510ad Merge tag '1.0.3' into develop
Release 1.0.3
2016-06-16 17:29:30 +12:00
Anthony Lapenna b940c7bfbd Merge branch 'release/1.0.3' 2016-06-16 17:29:25 +12:00
Anthony Lapenna 1460d69cd1 bump version number 2016-06-16 17:29:12 +12:00
Anthony Lapenna a7619b06ba configuration is now exposed in /config endpoint (#13) 2016-06-16 17:27:07 +12:00
Anthony Lapenna f3a5251fd4 Merge tag '1.0.2' into develop
Release 1.0.2
2016-06-14 14:36:31 +12:00
Anthony Lapenna 2ec247827d Merge branch 'release/1.0.2' 2016-06-14 14:36:26 +12:00
Anthony Lapenna e7a836a6b2 bump version number 2016-06-14 14:36:20 +12:00
Anthony Lapenna 6d163ee1ef add a refresh link for each entity view (#12) 2016-06-14 14:32:44 +12:00
Anthony Lapenna a12c1916ec switch to angular-ui-router (#11) 2016-06-14 14:13:52 +12:00
Anthony Lapenna 4e1a7077d7 Fix jshint error 2016-06-08 19:15:47 +12:00
Anthony Lapenna da453a6b7c Merge tag '1.0.1' into develop
Release 1.0.1
2016-06-08 18:36:52 +12:00
Anthony Lapenna 0230c5bb59 Merge branch 'release/1.0.1' 2016-06-08 18:36:42 +12:00
Anthony Lapenna a471b77f8d Bumped version number to 1.0.1 2016-06-08 18:36:35 +12:00
Anthony Lapenna b1e4800605 Merge branch 'chiu0602-master' into develop 2016-06-08 18:34:33 +12:00
Anthony Lapenna 7f5be16db8 merged 2016-06-08 18:34:26 +12:00
Anthony Lapenna 791e069a4c Added header directive and updated breadcrumb in each view (#8) 2016-06-08 18:23:11 +12:00
Anthony Lapenna 20bfca97e0 shipping minified scripts (#9) 2016-06-08 18:22:49 +12:00
Anthony Lapenna 3302c822f1 js/css files are now built under dist/js and dist/css 2016-06-08 10:09:37 +12:00
Anthony Lapenna 9a4115f086 Merge tag '1.0.0' into develop
Release 1.0.0
2016-06-02 18:14:15 +12:00
Anthony Lapenna f77a9b4db5 Merge branch 'release/1.0.0' 2016-06-02 18:14:10 +12:00
Anthony Lapenna b3b3feac66 Bump version number 2016-06-02 18:14:03 +12:00
Anthony Lapenna 324f36513e update documentation/naming 2016-06-02 18:09:18 +12:00
Anthony Lapenna ebf045d2e0 update LICENSE 2016-06-02 18:00:53 +12:00
Anthony Lapenna 3ec161ba56 udate .gitignore 2016-06-02 17:58:59 +12:00
Anthony Lapenna 12be43018e add missing package angular-ui-select 2016-06-02 17:58:30 +12:00
Anthony Lapenna 0f51cb66e0 Rdash theme integration (#1)
* Adding latest build to dist.

* Adding latest build to dist.

* Bump other app version.

* Build latest changes.

* Bump version to 0.7.0.

* Version bump to 0.9.0-beta and remote API 1.20.

* Whoah there, back down to 0.8.0-beta.

* Merge branch 'crosbymichael-master' into crosbymichael-dist

* Add volume options in volume creation form

* display swarm cluster information in Swarm tab

* update LICENSE

* update repository URL in status bar

* remove console logs

* do not display Swarm containers anywhere in the UI

* update position for add/remove option on Volumes page

* compliant with swarm == 1.2.0 API support

* update nginx-basic-auth examples with latest nginx and swarm example

* Updated .gitignore

* update .gitignore

* reverted entry for dist/uifordocker in .gitignore

* WIP

* fix linter issues

* added logo

* update repository URL

* update .gitignore (ignore dist/*)

* add lodash

* add containers actions binding (start, stop...)

* replace image icon

* bind remove image action

* bind network remove action

* bind volume remove action

* update logo

* wip on container details

* update logo scaling, favicon and page title

* wip container view

* add containers actions in container view

* add image view

* add network view

* remove useless data in tables

* add pull image, create network modals

* add create volume modal

* update style for createVolume options

* add start container modal

* create volume modal now use a select to display drivers

* add container stats

* add containerTop view in stats view

* fix trimcontainername filter

* add container logs view

* updated .gitignore

* remove useless files/modules

* remove useless chart in image view

* replace $location usage with $state.go

* remove useless swarm example
2016-06-02 17:34:03 +12:00
Kevan Ahlquist 1b206f223f DockerUI => UI For Docker 2016-04-29 21:57:27 -05:00
Kevan Ahlquist 02d4161ddd Merge pull request #205 from MSF-Jarvis/patch-1
Fix links
2016-04-27 08:39:03 -05:00
Harsh Shandilya 173c673d37 Fix links 2016-04-27 17:11:57 +05:30
Kevan Ahlquist 4fe6bcc5db Added manual build steps to quickstart. 2016-04-04 00:17:21 -05:00
Kevan Ahlquist 7e1d5338cf Merge pull request #201 from crosbymichael/csrf
Add CSRF Protection
2016-04-02 21:05:13 -05:00
Kevan Ahlquist a67206dd8d Version bump, 0.10.1-beta 2016-04-02 21:04:51 -05:00
Kevan Ahlquist 5f3d856535 Persist csrf authKey in container to allow restarts without breaking existing cookies. 2016-04-02 16:55:06 -05:00
Kevan Ahlquist 0244bc7317 Fix csrf, send tokens back in header, pass token instead of cookie back to server. 2016-04-02 14:39:30 -05:00
Kevan Ahlquist 7267516363 Don't re-generate token on every startup for now. 2016-04-01 11:10:30 -05:00
Kevan Ahlquist 15d133324d add gorilla/csrf
#199
2016-03-31 23:54:12 -05:00
Kevan Ahlquist 5bf922325a Sanitize text that gets sent to Gritter for notifications,
#198
2016-03-31 20:06:46 -05:00
Kevan Ahlquist b2b814a65b Merge pull request #196 from pmig/crosbymichael/dockerui#194
crosbymichael/dockerui#194 fix toggleAll function by only iterating over filtered objects
2016-03-25 09:40:24 -05:00
Philip Miglinci d2bc18b575 crosbymichael/dockerui#194 fix toggleAll function by only iterating over filtered objects
Signed-off-by: Philip Miglinci <p.miglinci@gmail.com>
2016-03-25 15:29:39 +01:00
Kevan Ahlquist 8cdb675abc Merge pull request #191 from erwin314/show-port-bindings2
Fixed: Show actual port bindings
2016-03-23 00:14:03 -05:00
Kevan Ahlquist a25829bbd9 Merge pull request #190 from erwin314/master
Fixed: Check if variable is plain object before using isEmptyObject()
2016-03-23 00:13:49 -05:00
Erwin Molendijk 194aa9a750 Fixed: Show actual port bindings 2016-03-21 11:13:21 +01:00
Erwin Molendijk e21af77246 Revert "Changed: Show automatic port bindings when PublishAllPorts is set."
This reverts commit bfe9038630.
2016-03-21 10:36:34 +01:00
Erwin Molendijk bfe9038630 Changed: Show automatic port bindings when PublishAllPorts is set. 2016-03-21 10:21:50 +01:00
Erwin Molendijk a33123469a Fixed: Check if variable is plain object before using isEmptyObject() 2016-03-21 09:25:40 +01:00
Kevan Ahlquist f353dc2c41 Add a global interceptor to catch 'conflict.' API responses. 2016-03-19 18:50:11 -05:00
Kevan Ahlquist a2a367725b Merge pull request #189 from crosbymichael/tables
Add column sorting to tables
2016-03-19 18:11:42 -05:00
Kevan Ahlquist db90a0eed7 Add column sorting to networks and volumes pages. 2016-03-19 18:09:17 -05:00
Kevan Ahlquist 93dba3f92f Add column sorting to images page. 2016-03-19 17:45:45 -05:00
Kevan Ahlquist 5a20b9fc04 Add column sorting to containers page. 2016-03-19 17:30:47 -05:00
Kevan Ahlquist 9793c3f3ee Fix jshint errors. 2016-03-07 00:07:46 -06:00
Kevan Ahlquist 6963a1ae8a Merge pull request #184 from ramzes642/master
Container edit feature
2016-03-03 00:46:01 -06:00
Roman Usachev da1a5ead39 Bugfixes 2016-02-24 10:07:59 +03:00
Roman Usachev f1b5037ee5 Host mode fix 2016-02-24 09:19:02 +03:00
Roman Usachev cf18a3cd60 Null vars fix 2016-02-24 07:16:07 +03:00
Roman Usachev cc1b67575c Null vars fix 2016-02-24 05:43:04 +03:00
Roman Usachev 50d33a07df Filesystem binds edit 2016-02-24 03:13:24 +03:00
Roman Usachev fc0dedfda7 Ports list now using HostConfig.PortBindings 2016-02-24 01:34:20 +03:00
Roman Usachev 35dbacdfff Port bindings edit, HostConfig fix 2016-02-23 20:47:21 +03:00
Roman Usachev ad0d23d686 Container env vars editing using commit and recreate 2016-02-22 09:10:16 +03:00
Kevan Ahlquist 786b94b285 Allow running containers to be committed. 2016-02-16 00:09:12 -06:00
Kevan Ahlquist 0ddf4a1828 Fix spec for top view. 2016-02-15 23:49:20 -06:00
Kevan Ahlquist b244242cb2 Add container name to stats and top pages. 2016-02-15 23:34:55 -06:00
Kevan Ahlquist 49d4c5800f Added swarm on docker-machine example. 2016-01-24 17:03:20 -06:00
Kevan Ahlquist a198382c06 Bump stats endpoint timeout to 5 seconds, retry on error. 2016-01-23 16:39:31 -06:00
Kevan Ahlquist 316017c516 Merge pull request #172 from chazmuzz/master
Use NetworkSettings.Ports rather than HostConfig.PortBindings
2016-01-18 23:34:09 -06:00
Charlie Murray f62cb483ce Use NetworkSettings.Ports rather than HostConfig.PortBindings to diplay host port bindings 2016-01-18 11:05:08 +00:00
Kevan Ahlquist 44c88f1ed5 Merge pull request #170 from kevana/stats-fix
Fix network stats error for 1.9.0+ clients, hide networks and volumes…
2016-01-10 22:00:38 -06:00
Kevan Ahlquist 9d1193c0b5 Fix network stats error for 1.9.0+ clients, hide networks and volumes better on older clients. 2016-01-10 21:51:51 -06:00
Kevan Ahlquist 22d35eb32f Merge pull request #162 from kevana/networks-volumes
Networks and Volumes
2016-01-10 20:39:14 -06:00
Kevan Ahlquist 2683ddb1ee Add refresh button. 2016-01-10 20:36:22 -06:00
Kevan Ahlquist fe7646e939 Use localStorage to only show 'learn more' banner on first load. 2016-01-10 20:02:04 -06:00
Kevan Ahlquist 00528edd7c Merge branch 'master' into networks-volumes 2016-01-10 19:34:03 -06:00
Michael Crosby 06c2ac4d5c Merge pull request #168 from linquize/chart-max-steps
Show maximum 10 steps of chart
2016-01-04 09:28:58 -08:00
Michael Crosby a447d64d83 Merge pull request #169 from linquize/desc
Change the description of DockerUI
2016-01-04 09:27:17 -08:00
Linquize 9260f23d41 DockerUI is the UI for Docker container engine
It is not only on Linux, but on Windows too.
2016-01-03 13:55:27 +08:00
Linquize d7082c3959 Show maximum 10 steps of chart 2016-01-03 12:59:43 +08:00
Kevan Ahlquist 437e171639 Merge pull request #163 from israelroldan/issue-158
Redirect to container list when removing a container (Fix for #158)
2015-12-26 17:54:23 -06:00
Israel Roldan 6de45cd422 Redirect to container list when removing a container (Fixes #158) 2015-12-24 00:04:47 -06:00
Kevan Ahlquist a065e0e259 Merge pull request #160 from crosbymichael/icons
Add custom logo, nginx basic auth docker-compose example
2015-12-20 21:25:34 -06:00
Kevan Ahlquist eede27e263 Implement volumes, hide networks and volumes tabs for APIs older than v1.21. 2015-12-20 21:17:01 -06:00
Kevan Ahlquist 8a900254ae Remove unused Dockerfile service, rename services to match API endpoint names: Docker => Version, System => Info. 2015-12-20 20:23:17 -06:00
Kevan Ahlquist 5f4641af67 Catch plaintext errors for creation and connection, implement network creation, 2015-12-20 20:07:57 -06:00
Kevan Ahlquist 8a4be8b93a Implemented single network view, connect, disconnect, remove. 2015-12-20 19:13:53 -06:00
Kevan Ahlquist a712d5b77e Progress on Networks. 2015-12-17 23:35:04 -06:00
Kevan Ahlquist 5947e262fc Added docker-compose config for nginx-basic-auth. 2015-12-17 01:31:07 -06:00
Kevan Ahlquist c3f22fe989 Add custom logo. 2015-12-17 00:08:29 -06:00
Kevan Ahlquist 2f003a5bb5 Merge pull request #159 from crosbymichael/tags-support
Support RepoTags for versions of the Docker API older than 1.21.
2015-12-14 22:24:41 -06:00
Kevan Ahlquist 75085b213a Support RepoTags for versions of the Docker API older than 1.21. 2015-12-13 23:28:28 -06:00
Kevan Ahlquist 91968b9f4b Switch to local jquery.gritter, fix undefined variable error in minified build due to "use strict"'; 2015-12-07 00:01:08 -06:00
Kevan Ahlquist d881d97c06 Merge pull request #155 from crosbymichael/labels
Labels and Image tags
2015-12-06 15:01:02 -06:00
Kevan Ahlquist 1dd1ce2f47 Allow tagging of repo and version and deletion of multiple tags. 2015-11-27 00:31:03 -06:00
Kevan Ahlquist 640c0b39c1 Add labels to container start and display. 2015-11-26 22:33:41 -06:00
Kevan Ahlquist 25a4607ad6 Merge pull request #154 from crosbymichael/restart-policy
Don't use self-closing select element
2015-11-26 21:15:02 -06:00
Kevan Ahlquist 064ffe2a7c Don't use self-closing select element, causes silent render error on Chrome. 2015-11-26 21:12:53 -06:00
Kevan Ahlquist 4d759fbfb9 Merge pull request #144 from linquize/padding
Remove body.padding-top, it wasted too much space
2015-10-11 14:38:33 -05:00
Linquize 9c854cb39d Remove body.padding-top, it wasted too much space 2015-10-11 21:13:04 +08:00
Kevan Ahlquist 27799e7033 Merge pull request #143 from linquize/scale
Fix containers, images chart scale too small
2015-10-10 16:33:43 -05:00
User 3ac1ac6086 Fix containers, images chart scale too small 2015-10-10 17:04:19 +08:00
Kevan Ahlquist 01db85ebc7 Merge pull request #140 from kevana/bower
Fix builds
2015-09-15 22:28:26 -05:00
Kevan Ahlquist cdf4767d7d Use grunt for building
Add shell commands to gruntfile for local dev.

Build binary if needed for `grunt build`, always build for `grunt release`. Exclude unused assets from jquery.gritter and vis.

Remove Makefile
2015-09-15 00:24:11 -05:00
Kevan Ahlquist 9bdd96527c Convert services.js for safe minification, preserve license comments in javascript files, add missing recess target, remove oboe-browser.min.js from assets. 2015-09-15 00:24:11 -05:00
Kevan Ahlquist 13e3fed351 Use bower for Angular dependencies. 2015-09-15 00:22:11 -05:00
Kevan Ahlquist 115a293e58 Merge pull request #13 from crosbymichael/master
Pull latest from origin
2015-09-13 16:18:12 -05:00
Kevan Ahlquist 9c985e63ee Merge pull request #135 from kevana/stats
Add view for container stats endpoint.
2015-09-09 22:58:37 -05:00
Kevan Ahlquist c57a75f78f Merge branch 'master' into stats
Conflicts:
	app/components/containers/containers.html
	app/components/images/images.html
2015-09-09 22:55:07 -05:00
Kevan Ahlquist a96298db59 Merge pull request #137 from dasJ/filter
Allow filtering containers and images
2015-09-09 22:48:19 -05:00
Kevan Ahlquist 061921af8e Merge pull request #136 from dasJ/editlabel
Rename save button when renaming container
2015-09-03 10:53:53 -05:00
Janne Heß 6719c84a7a Allow filtering images 2015-09-03 13:38:23 +02:00
Janne Heß 13f3335098 Allow filtering containers
Closes #84
2015-09-03 13:38:09 +02:00
Janne Heß a79c26a7a4 Rename save button when renaming container
Closes #120
2015-09-03 13:00:43 +02:00
Kevan Ahlquist abccb316f8 Updated docs links in services. 2015-08-29 02:38:03 -05:00
Kevan Ahlquist f14e954a9b Lower animation steps, keep 20 datapoints, update every 2 seconds. 2015-08-29 02:11:13 -05:00
Kevan Ahlquist 5d4a2a7c25 Fixed dashboard graphs, Chart.js doesn't like charts being hidden on creation, fixed routing issue to dashboard, version bump. 2015-08-29 01:57:08 -05:00
Kevan Ahlquist ce90515c95 Added network graph 2015-08-29 00:40:24 -05:00
Kevan Ahlquist 5a51495432 Added remaining memory stats, change humansize filter to give more accurate sizes. 2015-08-28 23:26:39 -05:00
Kevan Ahlquist b99fe5bf55 Added memory usage graph. 2015-08-27 01:41:45 -05:00
Kevan Ahlquist d243a83c5c Implemented CPU usage graph, upgraded to Chart.js 1.0.2, broke dashboard charts, tests. 2015-08-26 01:28:08 -05:00
Kevan Ahlquist 6cb658a1ef Fixed grunt style warnings. 2015-08-25 01:07:08 -05:00
Kevan Ahlquist 8a7f8f7c37 Whitespace diff, ran everything through the formatter. 2015-08-25 00:59:54 -05:00
Kevan Ahlquist b7daf91723 Current progress on stats page, nonfunctional. 2015-08-25 00:55:54 -05:00
Kevan Ahlquist 66894e7596 Merge pull request #133 from kgutwin/f-volume-network
Fix VolumesFrom display issue with Swarm, add option to view stopped containers in network tab.
2015-08-24 20:40:11 -05:00
Karl Gutwin 7748adf0e6 Color and edge cleanup 2015-08-12 16:38:50 -04:00
Karl Gutwin 758ac41648 Color and edge cleanup 2015-08-12 16:37:19 -04:00
Karl Gutwin 3db2008c39 Have to provide a color 2015-08-12 16:22:31 -04:00
Karl Gutwin 34a3f8186a Missed passing run state 2015-08-12 16:20:28 -04:00
Karl Gutwin 8e0baf0e37 Optionally include stopped containers 2015-08-12 16:01:33 -04:00
Karl Gutwin 017863bfc0 VolumesFrom support container names 2015-08-12 14:47:47 -04:00
Kevan Ahlquist f4ef63b8f8 Merge pull request #125 from hmichopoulos/dockerui-118
dockerui-118 Pull images / create container from repo image
2015-07-31 19:48:09 -05:00
Haris Michopoulos 394f2f2387 dockerui-118 How to pull images / create container from repo image ?
-Fixed ViewSpinner variable.
-Hide the modal on submit, show Spinner and show again modal if needed (in case of error)
-Clear modal values after successful pull
2015-06-22 23:30:16 +02:00
Haris Michopoulos 5dd094e0b1 dockerui-118 How to pull images / create container from repo image ?
Added a modal dialog that gets user's input (registry, repo, image name and tag) and performs the pull. Some hack was needed to parse the response, since it is not valid json (actually, it seems to be concatenated json objects).
2015-06-05 17:21:52 +02:00
Kevan Ahlquist 7d073fdf0d Merge pull request #100 from crosbymichael/events
Events
2015-05-28 16:33:36 -05:00
Kevan Ahlquist e806214d0b Merge branch 'master' into events 2015-05-20 23:44:32 -05:00
Kevan Ahlquist 419bf0aa77 Merge pull request #121 from cristiangauma/master
Adding protocol select menu in portBinding during container creation
2015-05-20 23:32:23 -05:00
Cristiangauma cad8a6dc91 Adding protocol select menu in portBinding during container creation 2015-05-19 22:42:24 +02:00
Kevan Ahlquist ef3596ff32 Merge branch 'master' into events
Conflicts:
	app/app.js
	index.html
2015-05-14 01:33:08 -05:00
Kevan Ahlquist 74dcec8b6b Merge pull request #12 from crosbymichael/master
Sync fork with latest master.
2015-05-14 01:19:42 -05:00
Kevan Ahlquist c0ae0d8d3f Merge branch 'crosbymichael-master' into bharat-add-commit
Conflicts:
	app/components/container/container.html
2015-05-14 01:01:26 -05:00
Kevan Ahlquist 4637c5fbbd Merge pull request #115 from BrockFredin/master
Restart Feature - Implementing restart feature for single containers
2015-05-14 00:50:26 -05:00
Fredin, Brock 74c92de1d3 Revert "DockerUI - Comment in Services was broken link. Updated the comment to include the most accurate link representing the Docker API reference material"
This reverts commit f167f5b49e.
2015-05-14 00:27:55 -05:00
Fredin, Brock 1688dbd8a0 Merge branch 'master' of https://github.com/BrockFredin/dockerui
* 'master' of https://github.com/BrockFredin/dockerui:
  Restart Feature - Implementing restart feature for single containers
2015-05-14 00:22:19 -05:00
Fredin, Brock f167f5b49e DockerUI - Comment in Services was broken link. Updated the comment to include the most accurate link representing the Docker API reference material 2015-05-14 00:21:53 -05:00
Bharath Thiruveedula 968efed6c3 Adding commit button 2015-05-12 22:23:19 +05:30
Fredin, Brock c10dc38a89 Restart Feature - Implementing restart feature for single containers 2015-05-12 00:52:12 -05:00
Kevan Ahlquist 2bc6b2995d Merge pull request #110 from rabelenda/master
Add controls for easy visualization when too many containers
2015-05-06 15:48:04 -05:00
Roger Abelenda edd6a41d6e Add actions to show upstream and downstream dependencies of a container for easy visualization.
Additionally updated angular-vis to use version 0.0.4 which already includes the changes of https://github.com/visjs/angular-visjs/pull/22.
2015-05-02 12:04:31 -03:00
Roger Abelenda 9e713b7b81 Add several
- Add search box to easily find containers in network. This required an addition to angular-vis which is in a pending pull request: https://github.com/visjs/angular-visjs/pull/22
- Add tooltip to easily visualize container id and image name.
- Add options to hide containers (and showAll to reset view) to ease visualization when there are too many containers.
2015-05-01 21:41:10 -03:00
Kevan Ahlquist 1f8f08e998 Merge pull request #109 from rabelenda/master
Fix code for lint checks and karma tests
2015-04-28 22:35:16 -05:00
Roger Abelenda 008884ec59 Change code to pass lint checks and add angular-vis to karma config for tests to pass. 2015-04-28 23:09:03 -03:00
Kevan Ahlquist 230bfd15ac Merge pull request #108 from rabelenda/master
Add a containers network tab, visualize links and volumesFrom relations between containers.
2015-04-26 02:19:52 -05:00
Roger Abelenda 7cda9a0c7b Add a containers network tab to be able to easily visualize links and volumesFrom relations between containers. 2015-04-25 14:02:24 -03:00
Kevan Ahlquist 6012453d2d Switch to ngOboe, for DI and promise interface. 2015-04-18 23:06:58 -07:00
Kevan Ahlquist 2514672c35 Merge commit '964eac6d530a7600459f92c312d79ee9365b0f9b' into events
Conflicts:
	app/app.js
	test/unit/app/components/containerController.spec.js
2015-04-18 21:26:15 -07:00
Kevan Ahlquist 964eac6d53 Merge pull request #102 from houssemba/master
Add Docker top feature
2015-04-02 09:40:01 -05:00
Houssem BELHADJ AHMED ede37e2e79 Add Docker top feature 2015-03-31 23:37:03 +02:00
Kevan Ahlquist 6080e097da Added date ranges to events. 2015-03-27 16:25:30 -05:00
Kevan Ahlquist 14bfc57870 Angular version bump to 1.3.15. 2015-03-27 15:16:44 -05:00
Kevan Ahlquist 32a4703a53 Added events endpoint. 2015-03-26 14:12:44 -05:00
Kevan Ahlquist d6c524f960 Merge pull request #99 from houssemba/master
Rename feature
2015-03-18 19:28:55 -05:00
Houssem BELHADJ AHMED c971189286 Add rename feature 2015-03-12 22:28:01 +01:00
Houssem BELHADJ AHMED f29eaa28ba Jshint & test fix 2015-03-08 18:19:51 +01:00
Kevan Ahlquist 9afe57e0ec Merge pull request #86 from kevana/master
Small fixes
2015-02-24 23:33:15 -06:00
Kevan Ahlquist fb0a0827b6 Merge commit '974ed1fe0598533747c2cf4c2f21d7830d484431'
Conflicts:
	app/components/containerLogs/containerLogsController.js
2015-02-24 23:32:38 -06:00
Kevan Ahlquist 22cdb94e5d Removed dist files. 2015-02-22 02:40:50 -06:00
Kevan Ahlquist 974ed1fe05 Merge pull request #77 from jonny64/logs_tail
logs page: slow on huge logs - 'show last lines' option
2015-02-21 16:03:17 -06:00
jonny64 66ba60c475 logs: added spinner during refresh 2015-02-21 18:09:12 +00:00
Kevan Ahlquist 65f7e32f94 Strip headers from logs before display, fixes #94 2015-02-18 22:56:55 -06:00
Kevan Ahlquist dd21a4025b Merge commit '1843836f229cadcdace5ca0c1b95fdb21dcfea2b'
Conflicts:
	dist/dockerui.js
	dist/templates/app.js
2015-02-18 22:46:37 -06:00
Kevan Ahlquist 1843836f22 Fixed release target in Makefile. 2015-02-18 01:05:51 -06:00
Kevan Ahlquist 44ba4e8bb8 Merge commit '22287e6a56c5a8824da94f2f6b9a464fa3da4192' 2015-02-18 00:55:40 -06:00
Kevan Ahlquist 2adaa33fdb Remove dist folder from master. 2015-02-17 22:48:23 -06:00
jonny64 b257216194 logs: reload button 2015-02-16 18:42:46 +00:00
jonny64 df5e7bc867 logs: removed debugging 2015-02-16 18:18:34 +00:00
jonny64 53041efaf1 logs: tail also STDERR 2015-02-16 17:42:02 +00:00
jonny64 87131f4ae3 Merge remote-tracking branch 'origin/master' into logs_tail 2015-02-16 17:37:04 +00:00
Kevan Ahlquist f66bcfd4ea Merge latest changes from crosbymichael/dockerui. 2015-02-14 14:23:48 -06:00
Kevan Ahlquist 1fb7338838 Merge pull request #90 from dasJ/dropdown
Add nice cursor for dropdown menus
2015-02-14 14:10:33 -06:00
Kevan Ahlquist da60a70f43 Merge pull request #92 from dasJ/image
Fix values on images page
2015-02-14 14:08:55 -06:00
Kevan Ahlquist 8848d2d53c Merge pull request #89 from dasJ/master
Improve settings page
2015-02-14 14:02:43 -06:00
Janne Heß ad6e0acab1 Correctly humanize RAM size on info page 2015-02-13 21:35:57 +01:00
Janne Heß 14d4397868 Rename settings page to info
You can't change any settings in the settings page, so the title is
missleading.
2015-02-13 21:35:57 +01:00
Janne Heß f32aadedfb Add more information to the settings page
Now shows all information we can get from the info object.
Exceptions: Name and NEventsListener, those don't seem to work.
2015-02-13 21:35:57 +01:00
Janne Heß b888081f54 Fix values on images page
Update to current Docker API and add virtual size of the image.
Also replace the deprecated Comment with some informations about the
build machine.
2015-02-13 21:22:08 +01:00
Kevan Ahlquist b00b61ce25 Merge pull request #91 from dasJ/checkbox
Make text next to "Display All" clickable
2015-02-13 09:10:07 -06:00
Janne Heß 9284e4b451 Make text next to "Display All" clickable 2015-02-13 14:32:03 +01:00
Janne Heß 716e25703e Add nice cursor for dropdown menus 2015-02-13 14:18:36 +01:00
Kevan Ahlquist 65d0b0110d Consume image IDs that contain slashes, fix container port labels, be nicer about old browsers. 2015-02-13 01:13:04 -06:00
Kevan Ahlquist 9baa85f2e4 Merge pull request #82 from kevana/master
Added restart to container list actions.
2015-02-13 01:00:32 -06:00
William McGann 22287e6a56 Using centurylink's golang-builder to create tiny docker image, going from ~400MB to 5.4MB. 2015-02-13 01:11:50 -05:00
Kevan Ahlquist 0d4274fdeb Added restart to container list actions. 2015-02-09 23:10:05 -06:00
Kevan Ahlquist 4d9d66e9f9 Merge pull request #81 from kevana/master
Add timestamp option to stderr
2015-02-09 23:02:01 -06:00
Kevan Ahlquist c7ebe9d881 Added timestamp option to stderr. 2015-02-09 23:00:34 -06:00
Kevan Ahlquist c798a95fb3 Merge pull request #9 from crosbymichael/master
Pull latest changes
2015-02-09 22:48:32 -06:00
Kevan Ahlquist d11c849a0f Merge pull request #79 from Slamper/master
Fix of HostConfig being lost when starting containers from the list
2015-02-09 22:45:00 -06:00
Hendrik Hofstadt 8abb445bd4 nicer solution for checking the action 2015-02-09 20:06:28 +01:00
Hendrik Hofstadt dabb1926a5 fixed all HostConfigs being lost when starting from the list 2015-02-08 19:59:26 +01:00
Hendrik Hofstadt 829ff5da73 fixed loss of hostconfig when starting with batch 2015-02-07 23:02:35 +01:00
jonny64 aead1fcc29 logs page: slow on huge logs - 'show last lines' option 2015-02-05 19:23:57 +00:00
Kevan Ahlquist ccd27f203d Merge pull request #76 from kevana/master
Fixes #75, HostConfig issues
2015-02-01 17:16:43 -06:00
Kevan Ahlquist 3d251fdba4 Fixes #75 2015-02-01 17:15:58 -06:00
Kevan Ahlquist 4b4edc6c22 Merge pull request #72 from kevana/master
Container start parameters
2015-02-01 01:35:37 -06:00
Kevan Ahlquist 990456dbdd Style tweaks for container start modal. 2015-01-31 14:42:10 -06:00
Kevan Ahlquist fd68039cb9 Added ExtraHosts option. 2015-01-28 19:26:48 -06:00
Kevan Ahlquist a80971dd27 Implemented RestartPolicy, Devices, and LxcConf. Unified label convention. 2015-01-28 02:28:41 -06:00
Kevan Ahlquist 687ed7bac2 Fixed Cmd parsing for strings, added error handling to start step. 2015-01-25 19:18:46 -06:00
Kevan Ahlquist 314fc51f6d Updated /dist for release. 2015-01-25 18:12:34 -06:00
Kevan Ahlquist f75d298fe1 Cleaned up controller methods, Added placeholders, fixed PortBindings and error messages. 2015-01-25 17:59:08 -06:00
Kevan Ahlquist 4682ae4ca7 Implemented remaining container creation options. 2015-01-25 15:52:40 -06:00
Kevan Ahlquist c8213bbf33 Added VolumesFrom to container start. 2015-01-20 23:31:57 -06:00
Kevan Ahlquist 4d22a04484 Added ENV variables to container start. 2015-01-20 01:44:30 -06:00
Kevan Ahlquist 76d7e280f9 Added PortBindings to container start. 2015-01-18 23:27:56 -06:00
Kevan Ahlquist 84b7da1164 Merge pull request #71 from kevana/master
Build and test automation
2015-01-18 16:51:39 -06:00
Kevan Ahlquist 77062bec84 Adding dist folder. 2015-01-18 16:40:07 -06:00
Kevan Ahlquist f65fed8d0a Fixed grunt template issues, version bump. 2015-01-18 16:39:10 -06:00
Kevan Ahlquist 2974f69932 Merge pull request #6 from kevana/feature/build
Build and test automation
2015-01-14 00:25:52 -06:00
Kevan Ahlquist 190087e6ca Added install and test targets to Makefile 2015-01-14 00:25:25 -06:00
Kevan Ahlquist 1784719047 Added unit tests for filters. 2015-01-04 20:35:47 -06:00
Kevan Ahlquist 275d771ea9 Merge branch 'master' into feature/build
Conflicts:
	.gitignore
2015-01-03 19:47:36 -06:00
Kevan Ahlquist f46b736c65 Merge pull request #7 from crosbymichael/master
Pull latest changes
2015-01-03 19:45:50 -06:00
Kevan Ahlquist 8eb381a899 Merge pull request #67 from crosbymichael/docs-update
Updated readme with new build info and more usage instructions.
2015-01-03 19:44:24 -06:00
Kevan Ahlquist 04418e2e68 Added Karma config for unit tests. 2015-01-03 18:59:22 -06:00
Kevan Ahlquist ab2819addd Cleaned up linter errors. 2015-01-03 18:39:40 -06:00
Kevan Ahlquist e507af62aa Merge branch 'master' into feature/build
Conflicts:
	app/app.js
	index.html
2015-01-03 17:01:02 -06:00
Kevan Ahlquist 468ba72253 Merge pull request #4 from crosbymichael/master
Pull latest changes
2015-01-03 16:55:56 -06:00
Kevan Ahlquist 58bd6faa47 Added initial files for build and test automation. 2015-01-03 16:49:28 -06:00
Kevan Ahlquist e601ae4370 Updated readme with new build info and more usage instructions. 2014-12-30 13:42:54 -06:00
Kevan Ahlquist 403daff934 Merge pull request #3 from kevana/bootstrap-update
Major refactoring
2014-12-11 15:29:16 -06:00
173 changed files with 14233 additions and 2564 deletions
+2
View File
@@ -0,0 +1,2 @@
*
!dist
+44
View File
@@ -0,0 +1,44 @@
<!--
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on gitter: https://gitter.im/portainer/Lobby.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
If you suspect your issue is a bug, please edit your issue description to
include the BUG REPORT INFORMATION shown below.
---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
You do NOT have to include this information if this is a FEATURE REQUEST
-->
**Description**
<!--
Briefly describe the problem you are having in a few paragraphs.
-->
**Steps to reproduce the issue:**
1.
2.
3.
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
**Technical details:**
* Portainer version:
* Portainer Docker image tag (latest/arm/windows...):
* Target Docker version (the host/cluster you manage):
* Target Swarm version (if applicable):
* Platform (windows/linux):
* Browser:
+6 -4
View File
@@ -1,4 +1,6 @@
logs/*
!.gitkeep
dockerui
*.esproj/*
node_modules
bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
+1 -1
View File
@@ -1 +1 @@
dockerui
portainer
+77
View File
@@ -0,0 +1,77 @@
# Contributing Guidelines
Some basic conventions for contributing to this project.
### General
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
* Develop in a topic branch, not master/develop
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
### Issues open to contribution
Want to contribute but don't know where to start?
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
* **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang
You can have a use Github filters to list these issues:
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
### Linting
Please check your code using `grunt lint` before submitting your pull requests.
### Commit Message Format
Each commit message should include a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(containers): add exposed ports in the containers view
#270 fix(templates): fix a display issue in the templates view
#269 style(dashboard): update dashboard with new layout
```
#### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
#### Scope
The scope could be anything specifying place of the commit change. For example `networks`,
`containers`, `images` etc...
You can use the **area** label tag associated on the issue here (for `area/containers` use `containers` as a scope...)
#### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
-7
View File
@@ -1,7 +0,0 @@
FROM crosbymichael/golang
ADD . /app/
WORKDIR /app/
RUN go build dockerui.go
EXPOSE 9000
ENTRYPOINT ["./dockerui"]
+56 -4
View File
@@ -1,7 +1,59 @@
DockerUI: Copyright (c) 2013-2014 Michael Crosby. crosbymichael.com
Portainer: Copyright (c) 2016 Portainer.io
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-19
View File
@@ -1,19 +0,0 @@
.PHONY: build run
.SUFFIXES:
OPEN = $(shell which xdg-open || which open)
PORT ?= 9000
build:
docker build --rm -t dockerui .
run:
-docker stop dockerui
-docker rm dockerui
docker run -d -p $(PORT):9000 -v /var/run/docker.sock:/docker.sock --name dockerui dockerui -e /docker.sock
open:
$(OPEN) localhost:$(PORT)
-1
View File
@@ -1 +0,0 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
+32 -55
View File
@@ -1,75 +1,52 @@
## DockerUI
![Containers](/containers.png)
DockerUI is a web interface to interact with the Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker. This project is not complete and is still under heavy development.
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
</p>
![Container](/container.png)
[![Microbadger version](https://images.microbadger.com/badges/version/portainer/portainer.svg)](https://microbadger.com/images/portainer/portainer "Latest version on Docker Hub")
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/latest/?badge=stable)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
### Goals
* Little to no dependencies - I really want to keep this project a pure html/js app. I know this will have to change so that I can introduce authentication and authorization along with managing multiple docker endpoints.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
### Container Quickstart
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.
* Run `docker build -t crosbymichael/dockerui github.com/crosbymichael/dockerui`
* `docker run -d -p 9000:9000 -v /var/run/docker.sock:/docker.sock crosbymichael/dockerui -e /docker.sock`
* Open your browser to `http://<dockerd host ip>:9000`
## Demo
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
Bind mounting the unix socket into the dockerui container is much more secure than exposing your docker
daemon over tcp. You should still secure your dockerui instance behind some type of auth. Maybe running
nginx infront of dockerui with basic auth.
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**).
### Connect via a unix socket
If you want to connect to docker via the unix socket you can pass the socket path to the `-e` variable. If you are running dockerui in a container you can bind mount the unix socket into the container.
Please note that the public demo cluster is **reset every 15min**.
```bash
docker run -d -p 9000:9000 -v /var/run/docker.sock:/docker.sock crosbymichael/dockerui -e /docker.sock
```
## Getting started
### Check the [wiki](//github.com/crosbymichael/dockerui/wiki) for more info about using dockerui
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
### Stack
* Angular.js
* Flatstrap ( Flat Twitter Bootstrap )
* Spin.js
* Ace editor
## Getting help
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Gitter (chat): https://gitter.im/portainer/Lobby
* Slack: http://portainer.io/slack/
### Todo:
* Full repository support
* Search
* Push files to a container
* Unit tests
## Reporting bugs and contributing
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
### License - MIT
The DockerUI code is licensed under the MIT license. Flatstrap(bootstrap) is licensed under the Apache License v2.0 and Angular.js is licensed under MIT.
## Limitations
**_Portainer_** has full support for the following Docker versions:
**DockerUI:**
Copyright (c) 2013 Michael Crosby. crosbymichael.com
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
* Docker Swarm >= 1.2.3
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Partial support for the following Docker versions (some features may not be available):
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* Docker 1.9
+72
View File
@@ -0,0 +1,72 @@
package bolt
import (
"time"
"github.com/boltdb/bolt"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
// Path where is stored the BoltDB database.
Path string
// Services
UserService *UserService
EndpointService *EndpointService
db *bolt.DB
}
const (
databaseFileName = "portainer.db"
userBucketName = "users"
endpointBucketName = "endpoints"
activeEndpointBucketName = "activeEndpoint"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) *Store {
store := &Store{
Path: storePath,
UserService: &UserService{},
EndpointService: &EndpointService{},
}
store.UserService.store = store
store.EndpointService.store = store
return store
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
path := store.Path + "/" + databaseFileName
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
if err != nil {
return err
}
return nil
})
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}
+174
View File
@@ -0,0 +1,174 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing users.
type EndpointService struct {
store *Store
}
const (
activeEndpointID = 0
)
// Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrEndpointNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpoint portainer.Endpoint
err = internal.UnmarshalEndpoint(data, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// Endpoints return an array containing all the endpoints.
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalEndpoint(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
if err != nil {
return nil, err
}
return endpoints, nil
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateEndpoint updates an endpoint.
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteEndpoint deletes an endpoint.
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
// GetActive returns the active endpoint.
func (service *EndpointService) GetActive() (*portainer.Endpoint, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(activeEndpointBucketName))
value := bucket.Get(internal.Itob(activeEndpointID))
if value == nil {
return portainer.ErrEndpointNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpoint portainer.Endpoint
err = internal.UnmarshalEndpoint(data, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// SetActive saves an endpoint as active.
func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(activeEndpointBucketName))
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(activeEndpointID), data)
if err != nil {
return err
}
return nil
})
}
// DeleteActive deletes the active endpoint.
func (service *EndpointService) DeleteActive() error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(activeEndpointBucketName))
err := bucket.Delete(internal.Itob(activeEndpointID))
if err != nil {
return err
}
return nil
})
}
+37
View File
@@ -0,0 +1,37 @@
package internal
import (
"github.com/portainer/portainer"
"encoding/binary"
"encoding/json"
)
// MarshalUser encodes a user to binary format.
func MarshalUser(user *portainer.User) ([]byte, error) {
return json.Marshal(user)
}
// UnmarshalUser decodes a user from a binary data.
func UnmarshalUser(data []byte, user *portainer.User) error {
return json.Unmarshal(data, user)
}
// MarshalEndpoint encodes an endpoint to binary format.
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
return json.Marshal(endpoint)
}
// UnmarshalEndpoint decodes an endpoint from a binary data.
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
+56
View File
@@ -0,0 +1,56 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// UserService represents a service for managing users.
type UserService struct {
store *Store
}
// User returns a user by username.
func (service *UserService) User(username string) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get([]byte(username))
if value == nil {
return portainer.ErrUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user portainer.User
err = internal.UnmarshalUser(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put([]byte(user.Username), data)
if err != nil {
return err
}
return nil
})
}
+61
View File
@@ -0,0 +1,61 @@
package cli
import (
"github.com/portainer/portainer"
"os"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
// Service implements the CLIService interface
type Service struct{}
const (
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
}
kingpin.Parse()
return flags, nil
}
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if *flags.Endpoint != "" {
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
return errInvalidEnpointProtocol
}
if strings.HasPrefix(*flags.Endpoint, "unix://") {
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
}
return err
}
}
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
// +build !windows
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
)
+12
View File
@@ -0,0 +1,12 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\ProgramData\\Portainer"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\ProgramData\\Portainer\\certs\\ca.pem"
defaultTLSCertPath = "C:\\ProgramData\\Portainer\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\ProgramData\\Portainer\\certs\\key.pem"
)
+40
View File
@@ -0,0 +1,40 @@
package cli
import (
"github.com/portainer/portainer"
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
type pairList []portainer.Pair
// Set implementation for a list of portainer.Pair
func (l *pairList) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
}
p := new(portainer.Pair)
p.Name = parts[0]
p.Value = parts[1]
*l = append(*l, *p)
return nil
}
// String implementation for a list of pair
func (l *pairList) String() string {
return ""
}
// IsCumulative implementation for a list of pair
func (l *pairList) IsCumulative() bool {
return true
}
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairList)(target))
return
}
+92
View File
@@ -0,0 +1,92 @@
package main // import "github.com/portainer/portainer"
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt"
"github.com/portainer/portainer/cli"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/jwt"
"log"
)
func main() {
var cli portainer.CLIService = &cli.Service{}
flags, err := cli.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal(err)
}
err = cli.ValidateFlags(flags)
if err != nil {
log.Fatal(err)
}
settings := &portainer.Settings{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
}
fileService, err := file.NewService(*flags.Data, "")
if err != nil {
log.Fatal(err)
}
var store = bolt.NewStore(*flags.Data)
err = store.Open()
if err != nil {
log.Fatal(err)
}
defer store.Close()
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
var cryptoService portainer.CryptoService = &crypto.Service{}
// Initialize the active endpoint from the CLI only if there is no
// active endpoint defined yet.
var activeEndpoint *portainer.Endpoint
if *flags.Endpoint != "" {
activeEndpoint, err = store.EndpointService.GetActive()
if err == portainer.ErrEndpointNotFound {
activeEndpoint = &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
TLS: *flags.TLSVerify,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
}
err = store.EndpointService.CreateEndpoint(activeEndpoint)
if err != nil {
log.Fatal(err)
}
} else if err != nil {
log.Fatal(err)
}
}
var server portainer.Server = &http.Server{
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
UserService: store.UserService,
EndpointService: store.EndpointService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
ActiveEndpoint: activeEndpoint,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
err = server.Start()
if err != nil {
log.Fatal(err)
}
}
+22
View File
@@ -0,0 +1,22 @@
package crypto
import (
"golang.org/x/crypto/bcrypt"
)
// Service represents a service for encrypting/hashing data.
type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hash), nil
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
func (*Service) CompareHashAndData(hash string, data string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(data))
}
+40
View File
@@ -0,0 +1,40 @@
package portainer
// General errors.
const (
ErrUnauthorized = Error("Unauthorized")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
)
// Endpoint errors.
const (
ErrEndpointNotFound = Error("Endpoint not found")
ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
)
// JWT errors.
const (
ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token")
)
// File errors.
const (
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
)
// Error represents an application error.
type Error string
// Error returns the error message.
func (e Error) Error() string { return string(e) }
+142
View File
@@ -0,0 +1,142 @@
package file
import (
"github.com/portainer/portainer"
"io"
"os"
"path"
"strconv"
)
const (
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
TLSStorePath = "tls"
// TLSCACertFile represents the name on disk for a TLS CA file.
TLSCACertFile = "ca.pem"
// TLSCertFile represents the name on disk for a TLS certificate file.
TLSCertFile = "cert.pem"
// TLSKeyFile represents the name on disk for a TLS key file.
TLSKeyFile = "key.pem"
)
// Service represents a service for managing files and directories.
type Service struct {
dataStorePath string
fileStorePath string
}
// NewService initializes a new service. It creates a data directory and a directory to store files
// inside this directory if they don't exist.
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
service := &Service{
dataStorePath: dataStorePath,
fileStorePath: path.Join(dataStorePath, fileStorePath),
}
// Checking if a mount directory exists is broken with Go on Windows.
// This will need to be reviewed after the issue has been fixed in Go.
// err := createDirectoryIfNotExist(dataStorePath, 0755)
// if err != nil {
// return nil, err
// }
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
if err != nil {
return nil, err
}
return service, nil
}
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
ID := strconv.Itoa(int(endpointID))
endpointStorePath := path.Join(TLSStorePath, ID)
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
if err != nil {
return err
}
var fileName string
switch fileType {
case portainer.TLSFileCA:
fileName = TLSCACertFile
case portainer.TLSFileCert:
fileName = TLSCertFile
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return portainer.ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(endpointStorePath, fileName)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return err
}
return nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) {
var fileName string
switch fileType {
case portainer.TLSFileCA:
fileName = TLSCACertFile
case portainer.TLSFileCert:
fileName = TLSCertFile
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return "", portainer.ErrUndefinedTLSFileType
}
ID := strconv.Itoa(int(endpointID))
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
}
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
ID := strconv.Itoa(int(endpointID))
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
err := os.RemoveAll(endpointPath)
if err != nil {
return err
}
return nil
}
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
path := path.Join(service.fileStorePath, name)
return createDirectoryIfNotExist(path, 0700)
}
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
func createDirectoryIfNotExist(path string, mode uint32) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
err = os.Mkdir(path, os.FileMode(mode))
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
// createFile creates a new file in the file store with the content from r.
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
path := path.Join(service.fileStorePath, filePath)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
if err != nil {
return err
}
return nil
}
+96
View File
@@ -0,0 +1,96 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// AuthHandler represents an HTTP API handler for managing authentication.
type AuthHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
}
const (
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler() *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.HandleFunc("/auth", h.handlePostAuth)
return h
}
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
var username = req.Username
var password = req.Password
u, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
tokenData := &portainer.TokenData{
username,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
}
type postAuthRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
type postAuthResponse struct {
JWT string `json:"jwt"`
}
+159
View File
@@ -0,0 +1,159 @@
package http
import (
"github.com/portainer/portainer"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"github.com/gorilla/mux"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
proxy http.Handler
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.proxyRequestsToDockerAPI(w, r)
})))
return h
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
if handler.proxy != nil {
handler.proxy.ServeHTTP(w, r)
} else {
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
}
}
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
var proxy http.Handler
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return err
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLS {
proxy, err = newHTTPSProxy(endpointURL, endpoint)
if err != nil {
return err
}
} else {
proxy = newHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = newSocketProxy(endpointURL.Path)
}
handler.proxy = proxy
return nil
}
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
// included here for use in NewSingleHostReverseProxyWithHostHeader
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
targetQuery := target.RawQuery
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
req.Host = req.URL.Host
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
}
return &httputil.ReverseProxy{Director: director}
}
func newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return NewSingleHostReverseProxyWithHostHeader(u)
}
func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https"
proxy := NewSingleHostReverseProxyWithHostHeader(u)
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
if err != nil {
return nil, err
}
proxy.Transport = &http.Transport{
TLSClientConfig: config,
}
return proxy, nil
}
func newSocketProxy(path string) http.Handler {
return &unixSocketHandler{path}
}
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
type unixSocketHandler struct {
path string
}
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := net.Dial("unix", h.path)
if err != nil {
Error(w, err, http.StatusInternalServerError, nil)
return
}
c := httputil.NewClientConn(conn, nil)
defer c.Close()
res, err := c.Do(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, nil)
return
}
defer res.Body.Close()
for k, vv := range res.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
if _, err := io.Copy(w, res.Body); err != nil {
Error(w, err, http.StatusInternalServerError, nil)
}
}
+309
View File
@@ -0,0 +1,309 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
type EndpointHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
FileService portainer.FileService
server *Server
middleWareService *middleWareService
}
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostEndpoints(w, r)
}))).Methods(http.MethodPost)
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetEndpoints(w, r)
}))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetEndpoint(w, r)
}))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePutEndpoint(w, r)
}))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleDeleteEndpoint(w, r)
}))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostEndpoint(w, r)
}))).Methods(http.MethodPost)
return h
}
// handleGetEndpoints handles GET requests on /endpoints
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, endpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
// /endpoints(?active=true|false)
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint := &portainer.Endpoint{
Name: req.Name,
URL: req.URL,
TLS: req.TLS,
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.TLS {
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
endpoint.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
activeEndpointParameter := r.FormValue("active")
if activeEndpointParameter != "" {
active, err := strconv.ParseBool(activeEndpointParameter)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
if active == true {
err = handler.server.updateActiveEndpoint(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
}
type postEndpointsRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
TLS bool
}
type postEndpointsResponse struct {
ID int `json:"Id"`
}
// handleGetEndpoint handles GET requests on /endpoints/:id
// GET /endpoints/0 returns active endpoint
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var endpoint *portainer.Endpoint
if id == "0" {
endpoint, err = handler.EndpointService.GetActive()
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if handler.server.ActiveEndpoint == nil {
err = handler.server.updateActiveEndpoint(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
} else {
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
encodeJSON(w, endpoint, handler.Logger)
}
// handlePostEndpoint handles POST requests on /endpoints/:id/active
func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.server.updateActiveEndpoint(endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointsRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: req.Name,
URL: req.URL,
TLS: req.TLS,
}
if req.TLS {
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
endpoint.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
endpoint.TLSKeyPath = keyPath
} else {
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putEndpointsRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
TLS bool
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
// DELETE /endpoints/0 deletes the active endpoint
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var endpoint *portainer.Endpoint
if id == "0" {
endpoint, err = handler.EndpointService.GetActive()
endpointID = int(endpoint.ID)
} else {
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
}
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if id == "0" {
err = handler.EndpointService.DeleteActive()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
}
}
+36
View File
@@ -0,0 +1,36 @@
package http
import (
"net/http"
"strings"
)
// FileHandler represents an HTTP API handler for managing static files.
type FileHandler struct {
http.Handler
}
func newFileHandler(assetPath string) *FileHandler {
h := &FileHandler{
Handler: http.FileServer(http.Dir(assetPath)),
}
return h
}
func isHTML(acceptContent []string) bool {
for _, accept := range acceptContent {
if strings.Contains(accept, "text/html") {
return true
}
}
return false
}
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !isHTML(r.Header["Accept"]) {
w.Header().Set("Cache-Control", "max-age=31536000")
} else {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
fileHandler.Handler.ServeHTTP(w, r)
}
+84
View File
@@ -0,0 +1,84 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"strings"
)
// Handler is a collection of all the service handlers.
type Handler struct {
AuthHandler *AuthHandler
UserHandler *UserHandler
EndpointHandler *EndpointHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler *FileHandler
}
const (
// ErrInvalidJSON defines an error raised the app is unable to parse request data
ErrInvalidJSON = portainer.Error("Invalid JSON")
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
)
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/auth") {
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/") {
h.FileHandler.ServeHTTP(w, r)
}
}
// Error writes an API error message to the response and logger.
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
// Log error.
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
}
// Write generic error response.
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
}
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// handleNotAllowed writes an API error message to the response and sets the Allow header.
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
}
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
if err := json.NewEncoder(w).Encode(v); err != nil {
Error(w, err, http.StatusInternalServerError, logger)
}
}
+63
View File
@@ -0,0 +1,63 @@
package http
import (
"github.com/portainer/portainer"
"net/http"
"strings"
)
// Service represents a service to manage HTTP middlewares
type middleWareService struct {
jwtService portainer.JWTService
}
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for _, mw := range middleware {
h = mw(h)
}
return h
}
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
h = service.middleWareSecureHeaders(h)
h = service.middleWareAuthenticate(h)
return h
}
// middleWareAuthenticate provides secure headers middleware for handlers
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}
// middleWareAuthenticate provides Authentication middleware for handlers
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
return
}
err := service.jwtService.VerifyToken(token)
if err != nil {
Error(w, err, http.StatusUnauthorized, nil)
return
}
next.ServeHTTP(w, r)
return
})
}
+85
View File
@@ -0,0 +1,85 @@
package http
import (
"github.com/portainer/portainer"
"net/http"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
UserService portainer.UserService
EndpointService portainer.EndpointService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
ActiveEndpoint *portainer.Endpoint
Handler *Handler
}
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
if endpoint != nil {
server.ActiveEndpoint = endpoint
server.Handler.WebSocketHandler.endpoint = endpoint
err := server.Handler.DockerHandler.setupProxy(endpoint)
if err != nil {
return err
}
err = server.EndpointService.SetActive(endpoint)
if err != nil {
return err
}
}
return nil
}
// Start starts the HTTP server
func (server *Server) Start() error {
middleWareService := &middleWareService{
jwtService: server.JWTService,
}
var authHandler = NewAuthHandler()
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService
var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.templatesURL = server.TemplatesURL
var dockerHandler = NewDockerHandler(middleWareService)
var websocketHandler = NewWebSocketHandler()
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
endpointHandler.server = server
var uploadHandler = NewUploadHandler(middleWareService)
uploadHandler.FileService = server.FileService
var fileHandler = newFileHandler(server.AssetsPath)
server.Handler = &Handler{
AuthHandler: authHandler,
UserHandler: userHandler,
EndpointHandler: endpointHandler,
SettingsHandler: settingsHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,
}
err := server.updateActiveEndpoint(server.ActiveEndpoint)
if err != nil {
return err
}
return http.ListenAndServe(server.BindAddress, server.Handler)
}
+40
View File
@@ -0,0 +1,40 @@
package http
import (
"github.com/portainer/portainer"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// SettingsHandler represents an HTTP API handler for managing settings.
type SettingsHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
settings *portainer.Settings
}
// NewSettingsHandler returns a new instance of SettingsHandler.
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
h := &SettingsHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.HandleFunc("/settings", h.handleGetSettings)
return h
}
// handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
return
}
encodeJSON(w, handler.settings, handler.Logger)
}
+53
View File
@@ -0,0 +1,53 @@
package http
import (
"io/ioutil"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// TemplatesHandler represents an HTTP API handler for managing templates.
type TemplatesHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
templatesURL string
}
// NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
h := &TemplatesHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetTemplates(w, r)
})))
return h
}
// handleGetTemplates handles GET requests on /templates
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
return
}
resp, err := http.Get(handler.templatesURL)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}
+26
View File
@@ -0,0 +1,26 @@
package http
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
)
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
return config, nil
}
+74
View File
@@ -0,0 +1,74 @@
package http
import (
"github.com/portainer/portainer"
"log"
"net/http"
"os"
"strconv"
"github.com/gorilla/mux"
)
// UploadHandler represents an HTTP API handler for managing file uploads.
type UploadHandler struct {
*mux.Router
Logger *log.Logger
FileService portainer.FileService
middleWareService *middleWareService
}
// NewUploadHandler returns a new instance of UploadHandler.
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
h := &UploadHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUploadTLS(w, r)
})))
return h
}
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
vars := mux.Vars(r)
endpointID := vars["endpointID"]
certificate := vars["certificate"]
ID, err := strconv.Atoi(endpointID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
file, _, err := r.FormFile("file")
defer file.Close()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
var fileType portainer.TLSFileType
switch certificate {
case "ca":
fileType = portainer.TLSFileCA
case "cert":
fileType = portainer.TLSFileCert
case "key":
fileType = portainer.TLSFileKey
default:
Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
}
+258
View File
@@ -0,0 +1,258 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
CryptoService portainer.CryptoService
middleWareService *middleWareService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUsers(w, r)
})))
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetUser(w, r)
}))).Methods(http.MethodGet)
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePutUser(w, r)
}))).Methods(http.MethodPut)
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUserPasswd(w, r)
})))
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postUsersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: req.Username,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
// handlePostUserPasswd handles POST requests on /users/:username/passwd
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
vars := mux.Vars(r)
username := vars["username"]
var req postUserPasswdRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
var password = req.Password
u, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
valid := true
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
valid = false
}
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
}
type postUserPasswdRequest struct {
Password string `valid:"required"`
}
type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
// handleGetUser handles GET requests on /users/:username
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
user, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// handlePutUser handles PUT requests on /users/:username
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
var req putUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: req.Username,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putUserRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
handleNotAllowed(w, []string{http.MethodGet})
return
}
user, err := handler.UserService.User("admin")
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// handlePostAdminInit handles POST requests on /users/admin/init
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postAdminInitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User("admin")
if err == portainer.ErrUserNotFound {
user := &portainer.User{
Username: "admin",
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
return
}
}
type postAdminInitRequest struct {
Password string `valid:"required"`
}
+185
View File
@@ -0,0 +1,185 @@
package http
import (
"github.com/portainer/portainer"
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"
"github.com/gorilla/mux"
"golang.org/x/net/websocket"
)
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
type WebSocketHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
endpoint *portainer.Endpoint
}
// NewWebSocketHandler returns a new instance of WebSocketHandler.
func NewWebSocketHandler() *WebSocketHandler {
h := &WebSocketHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/websocket/exec", websocket.Handler(h.webSocketDockerExec))
return h
}
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
qry := ws.Request().URL.Query()
execID := qry.Get("id")
// Should not be managed here
endpoint, err := url.Parse(handler.endpoint.URL)
if err != nil {
log.Fatalf("Unable to parse endpoint URL: %s", err)
return
}
var host string
if endpoint.Scheme == "tcp" {
host = endpoint.Host
} else if endpoint.Scheme == "unix" {
host = endpoint.Path
}
// Should not be managed here
var tlsConfig *tls.Config
if handler.endpoint.TLS {
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
handler.endpoint.TLSCertPath,
handler.endpoint.TLSKeyPath)
if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err)
return
}
}
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
log.Fatalf("error during hijack: %s", err)
return
}
}
type execConfig struct {
Tty bool
Detach bool
}
// hijack allows to upgrade an HTTP connection to a TCP connection
// It redirects IO streams for stdin, stdout and stderr to a websocket
func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
execConfig := &execConfig{
Tty: true,
Detach: false,
}
buf, err := json.Marshal(execConfig)
if err != nil {
return fmt.Errorf("error marshaling exec config: %s", err)
}
rdr := bytes.NewReader(buf)
req, err := http.NewRequest(method, path, rdr)
if err != nil {
return fmt.Errorf("error during hijack request: %s", err)
}
req.Header.Set("User-Agent", "Docker-Client")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
req.Host = addr
var (
dial net.Conn
dialErr error
)
if tlsConfig == nil {
dial, dialErr = net.Dial(scheme, addr)
} else {
dial, dialErr = tls.Dial(scheme, addr, tlsConfig)
}
if dialErr != nil {
return dialErr
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := dial.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
if err != nil {
return err
}
clientconn := httputil.NewClientConn(dial, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
clientconn.Do(req)
rwc, br := clientconn.Hijack()
defer rwc.Close()
if started != nil {
started <- rwc
}
var receiveStdout chan error
if stdout != nil || stderr != nil {
go func() (err error) {
if setRawTerminal && stdout != nil {
_, err = io.Copy(stdout, br)
}
return err
}()
}
go func() error {
if in != nil {
io.Copy(rwc, in)
}
if conn, ok := rwc.(interface {
CloseWrite() error
}); ok {
if err := conn.CloseWrite(); err != nil {
}
}
return nil
}()
if stdout != nil || stderr != nil {
if err := <-receiveStdout; err != nil {
return err
}
}
go func() {
for {
fmt.Println(br)
}
}()
return nil
}
+66
View File
@@ -0,0 +1,66 @@
package jwt
import (
"github.com/portainer/portainer"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/securecookie"
"time"
)
// Service represents a service for managing JWT tokens.
type Service struct {
secret []byte
}
type claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
func NewService() (*Service, error) {
secret := securecookie.GenerateRandomKey(32)
if secret == nil {
return nil, portainer.ErrSecretGeneration
}
service := &Service{
secret,
}
return service, nil
}
// GenerateToken generates a new JWT token.
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix()
cl := claims{
data.Username,
jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}
return signedToken, nil
}
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
func (service *Service) VerifyToken(token string) error {
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg
}
return service.secret, nil
})
if err != nil || parsedToken == nil || !parsedToken.Valid {
return portainer.ErrInvalidJWTToken
}
return nil
}
+132
View File
@@ -0,0 +1,132 @@
package portainer
import (
"io"
)
type (
// Pair defines a key/value string pair
Pair struct {
Name string `json:"name"`
Value string `json:"value"`
}
// CLIFlags represents the available flags on the CLI.
CLIFlags struct {
Addr *string
Assets *string
Data *string
Endpoint *string
Labels *[]Pair
Logo *string
Templates *string
TLSVerify *bool
TLSCacert *string
TLSCert *string
TLSKey *string
}
// Settings represents Portainer settings.
Settings struct {
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
}
// User represent a user account.
User struct {
Username string `json:"Username"`
Password string `json:"Password,omitempty"`
}
// TokenData represents the data embedded in a JWT token.
TokenData struct {
Username string
}
// EndpointID represents an endpoint identifier.
EndpointID int
// Endpoint represents a Docker endpoint with all the info required
// to connect to it.
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS"`
TLSCACertPath string `json:"TLSCACert,omitempty"`
TLSCertPath string `json:"TLSCert,omitempty"`
TLSKeyPath string `json:"TLSKey,omitempty"`
}
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
// It can be either a TLS CA file, a TLS certificate file or a TLS key file.
TLSFileType int
// CLIService represents a service for managing CLI.
CLIService interface {
ParseFlags(version string) (*CLIFlags, error)
ValidateFlags(flags *CLIFlags) error
}
// DataStore defines the interface to manage the data.
DataStore interface {
Open() error
Close() error
}
// Server defines the interface to serve the data.
Server interface {
Start() error
}
// UserService represents a service for managing users.
UserService interface {
User(username string) (*User, error)
UpdateUser(user *User) error
}
// EndpointService represents a service for managing endpoints.
EndpointService interface {
Endpoint(ID EndpointID) (*Endpoint, error)
Endpoints() ([]Endpoint, error)
CreateEndpoint(endpoint *Endpoint) error
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
DeleteEndpoint(ID EndpointID) error
GetActive() (*Endpoint, error)
SetActive(endpoint *Endpoint) error
DeleteActive() error
}
// CryptoService represents a service for encrypting/hashing data.
CryptoService interface {
Hash(data string) (string, error)
CompareHashAndData(hash string, data string) error
}
// JWTService represents a service for managing JWT tokens.
JWTService interface {
GenerateToken(data *TokenData) (string, error)
VerifyToken(token string) error
}
// FileService represents a service for managing files.
FileService interface {
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
DeleteTLSFiles(endpointID EndpointID) error
}
)
const (
// APIVersion is the version number of portainer API.
APIVersion = "1.11.2"
)
const (
// TLSFileCA represents a TLS CA certificate file.
TLSFileCA TLSFileType = iota
// TLSFileCert represents a TLS certificate file.
TLSFileCert
// TLSFileKey represents a TLS key file.
TLSFileKey
)
+570 -18
View File
@@ -1,19 +1,571 @@
'use strict';
angular.module('portainer', [
'portainer.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'ngFileUpload',
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'portainer.services',
'portainer.helpers',
'portainer.filters',
'auth',
'dashboard',
'container',
'containerConsole',
'containerLogs',
'containers',
'createContainer',
'docker',
'endpoint',
'endpointInit',
'endpoints',
'events',
'images',
'image',
'main',
'service',
'services',
'settings',
'sidebar',
'createService',
'stats',
'swarm',
'network',
'networks',
'node',
'createNetwork',
'task',
'templates',
'volumes',
'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
'use strict';
angular.module('dockerui', ['ngRoute', 'dockerui.services', 'dockerui.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'images', 'image', 'startContainer', 'sidebar', 'settings', 'builder', 'containerLogs'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {templateUrl: 'app/components/dashboard/dashboard.html', controller: 'DashboardController'});
$routeProvider.when('/containers/', {templateUrl: 'app/components/containers/containers.html', controller: 'ContainersController'});
$routeProvider.when('/containers/:id/', {templateUrl: 'app/components/container/container.html', controller: 'ContainerController'});
$routeProvider.when('/containers/:id/logs/', {templateUrl: 'app/components/containerLogs/containerlogs.html', controller: 'ContainerLogsController'});
$routeProvider.when('/images/', {templateUrl: 'app/components/images/images.html', controller: 'ImagesController'});
$routeProvider.when('/images/:id/', {templateUrl: 'app/components/image/image.html', controller: 'ImageController'});
$routeProvider.when('/settings', {templateUrl: 'app/components/settings/settings.html', controller: 'SettingsController'});
$routeProvider.otherwise({redirectTo: '/'});
}])
// This is your docker url that the api will use to make requests
// 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('UI_VERSION', 'v0.5')
.constant('DOCKER_API_VERSION', 'v1.15');
localStorageServiceProvider
.setStorageType('sessionStorage')
.setPrefix('portainer');
jwtOptionsProvider.config({
tokenGetter: ['LocalStorage', function(LocalStorage) {
return LocalStorage.getJWT();
}],
unauthenticatedRedirector: ['$state', function($state) {
$state.go('auth', {error: 'Your session has expired'});
}]
});
$httpProvider.interceptors.push('jwtInterceptor');
$urlRouterProvider.otherwise('/auth');
$stateProvider
.state('auth', {
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
"content": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
}
})
.state('containers', {
url: '/containers/',
views: {
"content": {
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('container', {
url: "^/containers/:id",
views: {
"content": {
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('stats', {
url: "^/containers/:id/stats",
views: {
"content": {
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('logs', {
url: "^/containers/:id/logs",
views: {
"content": {
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('console', {
url: "^/containers/:id/console",
views: {
"content": {
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('dashboard', {
url: '/dashboard',
views: {
"content": {
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions', {
abstract: true,
url: "/actions",
views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
})
.state('actions.create', {
abstract: true,
url: "/create",
views: {
"content": {
template: '<div ui-view="content"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
}
}
})
.state('actions.create.container', {
url: "/container",
views: {
"content": {
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.network', {
url: "/network",
views: {
"content": {
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.service', {
url: "/service",
views: {
"content": {
templateUrl: 'app/components/createService/createservice.html',
controller: 'CreateServiceController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.volume', {
url: "/volume",
views: {
"content": {
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('docker', {
url: '/docker/',
views: {
"content": {
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpoints', {
url: '/endpoints/',
views: {
"content": {
templateUrl: 'app/components/endpoints/endpoints.html',
controller: 'EndpointsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpoint', {
url: '^/endpoints/:id',
views: {
"content": {
templateUrl: 'app/components/endpoint/endpoint.html',
controller: 'EndpointController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpointInit', {
url: '/init/endpoint',
views: {
"content": {
templateUrl: 'app/components/endpointInit/endpointInit.html',
controller: 'EndpointInitController'
}
},
data: {
requiresLogin: true
}
})
.state('events', {
url: '/events/',
views: {
"content": {
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('images', {
url: '/images/',
views: {
"content": {
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('image', {
url: '^/images/:id/',
views: {
"content": {
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('networks', {
url: '/networks/',
views: {
"content": {
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('network', {
url: '^/networks/:id/',
views: {
"content": {
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('node', {
url: '^/nodes/:id/',
views: {
"content": {
templateUrl: 'app/components/node/node.html',
controller: 'NodeController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('services', {
url: '/services/',
views: {
"content": {
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('service', {
url: '^/service/:id/',
views: {
"content": {
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('settings', {
url: '/settings/',
views: {
"content": {
templateUrl: 'app/components/settings/settings.html',
controller: 'SettingsController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('task', {
url: '^/task/:id',
views: {
"content": {
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('templates', {
url: '/templates/',
views: {
"content": {
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('volumes', {
url: '/volumes/',
views: {
"content": {
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('swarm', {
url: '/swarm/',
views: {
"content": {
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
},
"sidebar": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
});
// The Docker API likes to return plaintext errors, this catches them and disp
$httpProvider.interceptors.push(function() {
return {
'response': function(response) {
if (typeof(response.data) === 'string' &&
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
$.gritter.add({
title: 'Error',
text: $('<div>').text(response.data).html(),
time: 10000
});
}
return response;
}
};
});
}])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', function ($rootScope, $state, Authentication, authManager, StateManager) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
StateManager.init();
$rootScope.$state = $state;
$rootScope.$on('tokenHasExpired', function($state) {
$state.go('auth', {error: 'Your session has expired'});
});
}])
// This is your docker url that the api will use to make requests
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
.constant('DOCKER_ENDPOINT', 'api/docker')
.constant('CONFIG_ENDPOINT', 'api/settings')
.constant('AUTH_ENDPOINT', 'api/auth')
.constant('USERS_ENDPOINT', 'api/users')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.11.2');
+101
View File
@@ -0,0 +1,101 @@
<div class="page-wrapper">
<!-- login box -->
<div class="container simple-box">
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
<!-- login box logo -->
<div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
</div>
<!-- !login box logo -->
<!-- init password panel -->
<div class="panel panel-default" ng-if="initPassword">
<div class="panel-body">
<!-- init password form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
Please specify a password for the <b>admin</b> user account.
</p>
</div>
<!-- !comment input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
Your password must be at least 8 characters long
</p>
</div>
<!-- !comment input -->
<!-- password input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
</div>
<!-- !password input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
Confirm your password
</p>
</div>
<!-- !comment input -->
<!-- password confirmation input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
</div>
<!-- !password confirmation input -->
<!-- validate button -->
<div class="form-group">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
</p>
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
</div>
</div>
<!-- !validate button -->
</form>
<!-- !init password form -->
</div>
</div>
<!-- !init password panel -->
<!-- login panel -->
<div class="panel panel-default" ng-if="!initPassword">
<div class="panel-body">
<!-- login form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
<!-- username input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
</div>
<!-- !username input -->
<!-- password input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
</div>
<!-- !password input -->
<!-- login button -->
<div class="form-group">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
</p>
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
</div>
</div>
<!-- !login button -->
</form>
<!-- !login form -->
</div>
</div>
<!-- !login panel -->
</div>
</div>
<!-- !login box -->
</div>
+80
View File
@@ -0,0 +1,80 @@
angular.module('auth', [])
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) {
$scope.authData = {
username: 'admin',
password: '',
error: ''
};
$scope.initPasswordData = {
password: '',
password_confirmation: '',
error: false
};
if ($stateParams.logout) {
Authentication.logout();
}
if ($stateParams.error) {
$scope.authData.error = $stateParams.error;
Authentication.logout();
}
if (Authentication.isAuthenticated()) {
$state.go('dashboard');
}
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
Users.checkAdminUser({}, function (d) {},
function (e) {
if (e.status === 404) {
$scope.initPassword = true;
} else {
Messages.error("Failure", e, 'Unable to verify administrator account existence');
}
});
$scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) {
$scope.initPassword = false;
$timeout(function() {
var element = $window.document.getElementById('password');
if(element) {
element.focus();
}
});
}, function (e) {
$scope.initPassword.error = true;
});
};
$scope.authenticateUser = function() {
$scope.authenticationError = false;
var username = $sanitize($scope.authData.username);
var password = $sanitize($scope.authData.password);
Authentication.login(username, password).then(function success() {
EndpointService.getActive().then(function success(data) {
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
}, function error(err) {
if (err.status === 404) {
$state.go('endpointInit');
} else {
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
}
});
}, function error() {
$scope.authData.error = 'Invalid credentials';
});
};
}]);
-17
View File
@@ -1,17 +0,0 @@
<div id="build-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Build Image</h3>
</div>
<div class="modal-body">
<div id="editor"></div>
<p>{{ messages }}</p>
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary" ng-click="build()">Build</a>
</div>
</div>
</div>
</div>
@@ -1,5 +0,0 @@
angular.module('builder', [])
.controller('BuilderController', ['$scope', 'Dockerfile', 'Messages',
function($scope, Dockerfile, Messages) {
$scope.template = 'app/components/builder/builder.html';
}]);
+264 -128
View File
@@ -1,131 +1,267 @@
<div class="detail">
<h4>Container: {{ container.Name }}</h4>
<rd-header>
<rd-header-title title="Container details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>
</rd-header>
<div class="btn-group detail">
<button class="btn btn-success"
ng-click="start()"
ng-show="!container.State.Running">Start</button>
<button class="btn btn-warning"
ng-click="stop()"
ng-show="container.State.Running && !container.State.Paused">Stop</button>
<button class="btn btn-danger"
ng-click="kill()"
ng-show="container.State.Running && !container.State.Paused">Kill</button>
<button class="btn btn-info"
ng-click="pause()"
ng-show="container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-success"
ng-click="unpause()"
ng-show="container.State.Running && container.State.Paused">Unpause</button>
</div>
<table class="table table-striped">
<tbody>
<tr>
<td>Created:</td>
<td>{{ container.Created }}</td>
</tr>
<tr>
<td>Path:</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args:</td>
<td>{{ container.Args }}</td>
</tr>
<tr>
<td>Exposed Ports:</td>
<td>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
</td>
</tr>
<tr>
<td>Environment:</td>
<td>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
</tr>
<tr>
<td>Publish All:</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
<tr>
<td>Ports:</td>
<td>
<ul style="display:inline-table">
<li ng-repeat="(containerport, hostports) in container.HostConfig.PortBindings">
{{ containerport }} => <span class="label" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
</li>
</ul>
</td>
</tr>
<tr>
<td>Hostname:</td>
<td>{{ container.Config.Hostname }}</td>
</tr>
<tr>
<td>IPAddress:</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Cmd:</td>
<td>{{ container.Config.Cmd }}</td>
</tr>
<tr>
<td>Entrypoint:</td>
<td>{{ container.Config.Entrypoint }}</td>
</tr>
<tr>
<td>Volumes:</td>
<td>{{ container.Volumes }}</td>
</tr>
<tr>
<td>SysInitpath:</td>
<td>{{ container.SysInitPath }}</td>
</tr>
<tr>
<td>Image:</td>
<td><a href="#/images/{{ container.Image }}/">{{ container.Image }}</a></td>
</tr>
<tr>
<td>State:</td>
<td><span class="label {{ container.State|getstatelabel }}">{{ container.State|getstatetext }}</span></td>
</tr>
<tr>
<td>Logs:</td>
<td><a href="#/containers/{{ container.Id }}/logs">stdout/stderr</a></td>
</tr>
</tbody>
</table>
<div class="row-fluid">
<div class="span1">
Changes:
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
<rd-widget-body classes="padding">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="span5">
<i class="icon-refresh" style="width:32px;height:32px;" ng-click="getChanges()"></i>
</div>
</div>
<div class="well well-large">
<ul>
<li ng-repeat="change in changes | filter:hasContent">
<strong>{{ change.Path }}</strong> {{ change.Kind }}
</li>
</ul>
</div>
<hr />
<div class="btn-remove">
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="remove()">Remove Container</button>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!container.edit">
{{ container.Name|trimcontainername }}
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="container.edit">
<form ng-submit="renameContainer()">
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</form>
</td>
</tr>
<tr ng-if="container.NetworkSettings.IPAddress">
<td>IP address</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Status</td>
<td>
<i ng-class="{true: 'fa fa-heartbeat space-right green-icon', false: 'fa fa-heartbeat space-right red-icon'}[container.State.Running]"></i>
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
</td>
</tr>
<tr ng-if="container.State.Running">
<td>Start time</td>
<td>{{ container.State.StartedAt|getisodate }}</td>
</tr>
<tr ng-if="!container.State.Running">
<td>Finished</td>
<td>{{ container.State.FinishedAt|getisodate }}</td>
</tr>
<tr>
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Create image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- tag-description -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You can create an image from this container, this allows you to backup important data or save
helpful configurations. You'll be able to spin up another container based on this image afterward.
</span>
</div>
</div>
<!-- !tag-description -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</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">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</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.Image" ng-click="commit()">Create</button>
<i id="createImageSpinner" 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="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
<tr ng-if="portBindings.length > 0">
<td>Port configuration</td>
<td>
<div ng-repeat="portMapping in portBindings">
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
</div>
</td>
</tr>
<tr>
<td>CMD</td>
<td><code>{{ container.Config.Cmd|command }}</code></td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in container.Config.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="!(container.Config.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="container.HostConfig.RestartPolicy.Name !== 'no'">
<td>Restart policies</td>
<td>
<table class="table table-bordered table-condensed">
<tr>
<td class="col-md-3">Name</td>
<td>{{ container.HostConfig.RestartPolicy.Name }}</td>
</tr>
<tr>
<td class="col-md-3">MaximumRetryCount</td>
<td>
{{ container.HostConfig.RestartPolicy.MaximumRetryCount }}
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="vol in container.HostConfig.Binds">
<td>{{ vol|key: ':' }}</td>
<td>{{ vol|value: ':' }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Connected networks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<th>Network Name</th>
<th>IP Address</th>
<th>Gateway</th>
<th>MacAddress</th>
<th>Actions</th>
</thead>
<tbody>
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: state.pagination_count">
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td>
<td>
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
</td>
</tr>
</tbody>
</table>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+167 -92
View File
@@ -1,105 +1,180 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'ViewSpinner',
function($scope, $routeParams, $location, Container, Messages, ViewSpinner) {
$scope.changes = [];
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('container_networks');
var update = function() {
ViewSpinner.spin();
Container.get({id: $routeParams.id}, function(d) {
$scope.container = d;
ViewSpinner.stop();
}, function(e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('container_networks', $scope.state.pagination_count);
};
var update = function () {
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
if (d.State.Running) {
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
} else {
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
}
$scope.portBindings = [];
if (d.NetworkSettings.Ports) {
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
if (d.NetworkSettings.Ports[portMapping]) {
var mapping = {};
mapping.container = portMapping;
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
$scope.portBindings.push(mapping);
}
});
};
}
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
};
$scope.start = function(){
ViewSpinner.spin();
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
}, function(d) {
update();
Messages.send("Container started", $routeParams.id);
}, function(e) {
update();
Messages.error("Failure", "Container failed to start." + e.data);
});
};
$scope.start = function () {
$('#loadingViewSpinner').show();
Container.start({id: $scope.container.Id}, {}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to start container");
});
};
$scope.stop = function() {
ViewSpinner.spin();
Container.stop({id: $routeParams.id}, function(d) {
update();
Messages.send("Container stopped", $routeParams.id);
}, function(e) {
update();
Messages.error("Failure", "Container failed to stop." + e.data);
});
};
$scope.stop = function () {
$('#loadingViewSpinner').show();
Container.stop({id: $stateParams.id}, function (d) {
update();
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to stop container");
});
};
$scope.kill = function() {
ViewSpinner.spin();
Container.kill({id: $routeParams.id}, function(d) {
update();
Messages.send("Container killed", $routeParams.id);
}, function(e) {
update();
Messages.error("Failure", "Container failed to die." + e.data);
});
};
$scope.kill = function () {
$('#loadingViewSpinner').show();
Container.kill({id: $stateParams.id}, function (d) {
update();
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to kill container");
});
};
$scope.pause = function() {
ViewSpinner.spin();
Container.pause({id: $routeParams.id}, function(d) {
update();
Messages.send("Container paused", $routeParams.id);
}, function(e) {
update();
Messages.error("Failure", "Container failed to pause." + e.data);
});
};
$scope.commit = function () {
$('#createImageSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
update();
Messages.send("Container commited", $stateParams.id);
}, function (e) {
$('#createImageSpinner').hide();
update();
Messages.error("Failure", e, "Unable to commit container");
});
};
$scope.unpause = function() {
ViewSpinner.spin();
Container.unpause({id: $routeParams.id}, function(d) {
update();
Messages.send("Container unpaused", $routeParams.id);
}, function(e) {
update();
Messages.error("Failure", "Container failed to unpause." + e.data);
});
};
$scope.pause = function () {
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to pause container");
});
};
$scope.remove = function() {
ViewSpinner.spin();
Container.remove({id: $routeParams.id}, function(d) {
update();
Messages.send("Container removed", $routeParams.id);
}, function(e){
update();
Messages.error("Failure", "Container failed to remove." + e.data);
});
};
$scope.unpause = function () {
$('#loadingViewSpinner').show();
Container.unpause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to unpause container");
});
};
$scope.hasContent = function(data) {
return data !== null && data !== undefined;
};
$scope.remove = function () {
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", d.message);
}
else {
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}
}, function (e) {
update();
Messages.error("Failure", e, "Unable to remove container");
});
};
$scope.getChanges = function() {
ViewSpinner.spin();
Container.changes({id: $routeParams.id}, function(d) {
$scope.changes = d;
ViewSpinner.stop();
});
};
$scope.restart = function () {
$('#loadingViewSpinner').show();
Container.restart({id: $stateParams.id}, function (d) {
update();
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to restart container");
});
};
update();
$scope.getChanges();
$scope.renameContainer = function () {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Unable to rename container", {}, d.message);
} else {
$scope.container.Name = $scope.container.newContainerName;
Messages.send("Container successfully renamed", d.name);
}
}, function (e) {
Messages.error("Failure", e, 'Unable to rename container');
});
$scope.container.edit = false;
};
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
$('#loadingViewSpinner').show();
Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Container left network", $stateParams.id);
$state.go('container', {id: $stateParams.id}, {reload: true});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container from network");
});
};
update();
}]);
@@ -0,0 +1,52 @@
<rd-header>
<rd-header-title title="Container console">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content ng-if="state.loaded">
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-terminal" title="Console">
<div class="pull-right">
<i id="loadConsoleSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px; display: none;"></i>
</div>
</rd-widget-header>
<rd-widget-body>
<form>
<div class="row">
<!-- command-list -->
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
</span>
<select class="form-control" ng-model="state.command" id="command">
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
</select>
</div>
</div>
<!-- !command-list -->
<div class="col-sm-8">
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div id="terminal-container" class="terminal-container"></div>
</div>
</div>
@@ -0,0 +1,121 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'Messages',
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Messages) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
var socket, term;
// Ensure the socket is closed before leaving the view
$scope.$on('$stateChangeStart', function (event, next, current) {
if (socket && socket !== null) {
socket.close();
}
});
Container.get({id: $stateParams.id}, function(d) {
$scope.container = d;
if (d.message) {
Messages.error("Error", d, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide();
} else {
Image.get({id: d.Image}, function(imgData) {
$scope.imageOS = imgData.Os;
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve image details');
$('#loadingViewSpinner').hide();
});
}
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide();
});
$scope.connect = function() {
$('#loadConsoleSpinner').show();
var termWidth = Math.round($('#terminal-container').width() / 8.2);
var termHeight = 30;
var execConfig = {
id: $stateParams.id,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: $scope.state.command.replace(" ", ",").split(",")
};
Container.exec(execConfig, function(d) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Messages.error("Error", {}, d.message);
} else {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId;
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
}
}, function (e) {
$('#loadConsoleSpinner').hide();
Messages.error("Failure", e, 'Unable to start an exec instance');
});
};
$scope.disconnect = function() {
$scope.state.connected = false;
if (socket !== null) {
socket.close();
}
if (term !== null) {
term.destroy();
}
};
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
if (d.message) {
Messages.error('Error', {}, 'Unable to resize TTY');
}
}, function (e) {
Messages.error("Failure", {}, 'Unable to resize TTY');
});
}, 2000);
}
function initTerm(url, height, width) {
socket = new WebSocket(url);
$scope.state.connected = true;
socket.onopen = function(evt) {
$('#loadConsoleSpinner').hide();
term = new Terminal();
term.on('data', function (data) {
socket.send(data);
});
term.open(document.getElementById('terminal-container'));
term.resize(width, height);
term.setOption('cursorBlink', true);
socket.onmessage = function (e) {
term.write(e.data);
};
socket.onerror = function (error) {
$scope.state.connected = false;
};
socket.onclose = function(evt) {
$scope.state.connected = false;
};
};
}
}]);
@@ -1,48 +1,75 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
function($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
$scope.stdout = '';
$scope.stderr = '';
$scope.showTimestamps = false;
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
$scope.stdout = '';
$scope.stderr = '';
$scope.tailLines = 2000;
ViewSpinner.spin();
Container.get({id: $routeParams.id}, function(d) {
$scope.container = d;
ViewSpinner.stop();
}, function(e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
function getLogs() {
$('#loadingViewSpinner').show();
getLogsStdout();
getLogsStderr();
$('#loadingViewSpinner').hide();
}
function getLogsStderr() {
ContainerLogs.get($stateParams.id, {
stdout: 0,
stderr: 1,
timestamps: $scope.state.displayTimestampsErr,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stderr = data;
});
}
function getLogs() {
ContainerLogs.get($routeParams.id, {stdout: 1, stderr: 0, timestamps: $scope.showTimestamps}, function(data, status, headers, config) {
// Replace carriage returns twith newlines to clean up output
$scope.stdout = data.replace(/[\r]/g, '\n');
});
ContainerLogs.get($routeParams.id, {stdout: 0, stderr: 1}, function(data, status, headers, config) {
$scope.stderr = data.replace(/[\r]/g, '\n');
});
}
// initial call
getLogs();
var logIntervalId = window.setInterval(getLogs, 5000);
$scope.$on("$destroy", function(){
// clearing interval when view changes
clearInterval(logIntervalId);
function getLogsStdout() {
ContainerLogs.get($stateParams.id, {
stdout: 1,
stderr: 0,
timestamps: $scope.state.displayTimestampsOut,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stdout = data;
});
}
$scope.scrollTo = function(id) {
$location.hash(id);
$anchorScroll();
}
// initial call
getLogs();
var logIntervalId = window.setInterval(getLogs, 5000);
$scope.toggleTimestamps = function() {
getLogs();
}
$scope.$on("$destroy", function () {
// clearing interval when view changes
clearInterval(logIntervalId);
});
$scope.toggleTimestampsOut = function () {
getLogsStdout();
};
$scope.toggleTimestampsErr = function () {
getLogsStderr();
};
}]);
+53 -31
View File
@@ -1,34 +1,56 @@
<div class="row logs">
<div class="col-xs-12">
<h4>Logs for container: <a href="#/containers/{{ container.Id }}/">{{ container.Name }}</a></td></h4>
<div class="btn-group detail">
<button class="btn btn-info" ng-click="scrollTo('stdout')">stdout</button>
<button class="btn btn-warning" ng-click="scrollTo('stderr')">stderr</button>
</div>
<div class="pull-right">
<input id="timestampToggle" type="checkbox" ng-model="showTimestamps"
ng-change="toggleTimestamps()"/> <label for="timestampToggle">Display Timestamps</label>
</div>
</div>
<rd-header>
<rd-header-title title="Container logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>
</rd-header>
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 id="stdout" class="panel-title">STDOUT</h3>
</div>
<div class="panel-body">
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
</div>
</div>
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 id="stderr" class="panel-title">STDERR</h3>
</div>
<div class="panel-body">
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
</div>
</div>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
<label for="displayAllTsOut">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
<label for="displayAllTsErr">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+123 -43
View File
@@ -1,45 +1,125 @@
<rd-header>
<rd-header-title title="Container list">
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadContainersSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Containers</rd-header-content>
</rd-header>
<h2>Containers:</h2>
<div>
<ul class="nav nav-pills pull-left">
<li class="dropdown">
<a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" data-target="#">Actions <b class="caret"></b></a>
<ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
<li><a tabindex="-1" href="" ng-click="startAction()">Start</a></li>
<li><a tabindex="-1" href="" ng-click="stopAction()">Stop</a></li>
<li><a tabindex="-1" href="" ng-click="killAction()">Kill</a></li>
<li><a tabindex="-1" href="" ng-click="pauseAction()">Pause</a></li>
<li><a tabindex="-1" href="" ng-click="unpauseAction()">Unpause</a></li>
<li><a tabindex="-1" href="" ng-click="removeAction()">Remove</a></li>
</ul>
</li>
</ul>
<div class="pull-right">
<input type="checkbox" ng-model="displayAll"
ng-change="toggleGetAll()"/> Display All
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Containers">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-primary btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<a class="btn btn-default btn-responsive" 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="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="containers" ng-click="order('Status')">
State
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Names')">
Name
<span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="state.displayIP">
<a ui-sref="containers" ng-click="order('IP')">
IP Address
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Ports')">
Published Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
<td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
</a>
<span ng-if="container.Ports.length == 0" >-</span>
</td>
</tr>
<tr ng-if="!containers">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="containers.length == 0">
<td colspan="8" class="text-center text-muted">No containers available.</td>
</tr>
</tbody>
</table>
<div ng-if="containers" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
<table class="table table-striped">
<thead>
<tr>
<th><input type="checkbox" ng-model="toggle" ng-change="toggleSelectAll()" /> Action</th>
<th>Name</th>
<th>Image</th>
<th>Command</th>
<th>Created</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="container in containers|orderBy:predicate">
<td><input type="checkbox" ng-model="container.Checked" /></td>
<td><a href="#/containers/{{ container.Id }}/">{{ container|containername}}</a></td>
<td><a href="#/images/{{ container.Image }}/">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:40 }}</td>
<td>{{ container.Created|getdate }}</td>
<td><span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span></td>
</tr>
</tbody>
</table>
+177 -68
View File
@@ -1,81 +1,190 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner',
function($scope, Container, Settings, Messages, ViewSpinner) {
$scope.predicate = '-Created';
$scope.toggle = false;
$scope.displayAll = Settings.displayAll;
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination',
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.state.selectedItemCount = 0;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
var update = function(data) {
ViewSpinner.spin();
Container.query(data, function(d) {
$scope.containers = d.map(function(item) {
return new ContainerViewModel(item); });
ViewSpinner.stop();
});
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
var batch = function(items, action, msg) {
ViewSpinner.spin();
var counter = 0;
var complete = function() {
counter = counter -1;
if (counter === 0) {
ViewSpinner.stop();
update({all: Settings.displayAll ? 1 : 0});
}
};
angular.forEach(items, function(c) {
if (c.Checked) {
counter = counter + 1;
action({id: c.Id}, function(d) {
Messages.send("Container " + msg, c.Id);
var index = $scope.containers.indexOf(c);
complete();
}, function(e) {
Messages.error("Failure", e.data);
complete();
});
}
});
if (counter === 0) {
ViewSpinner.stop();
var update = function (data) {
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status);
if (model.IP) {
$scope.state.displayIP = true;
}
};
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
}
return model;
});
$('#loadContainersSpinner').hide();
}, function (e) {
$('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers");
$scope.containers = [];
});
};
$scope.toggleSelectAll = function() {
angular.forEach($scope.containers, function(i) {
i.Checked = $scope.toggle;
});
};
$scope.toggleGetAll = function() {
Settings.displayAll = $scope.displayAll;
var batch = function (items, action, msg) {
$('#loadContainersSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadContainersSpinner').hide();
update({all: Settings.displayAll ? 1 : 0});
}
};
angular.forEach(items, function (c) {
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e, "Unable to start container");
complete();
});
}
else if (action === Container.remove) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
}
else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove container');
complete();
});
}
else if (action === Container.pause) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Container is already paused", c.Id);
} else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to pause container');
complete();
});
}
else {
action({id: c.Id}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e, 'An error occured');
complete();
});
$scope.startAction = function() {
batch($scope.containers, Container.start, "Started");
};
}
}
});
if (counter === 0) {
$('#loadContainersSpinner').hide();
}
};
$scope.stopAction = function() {
batch($scope.containers, Container.stop, "Stopped");
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredContainers, function (container) {
if (container.Checked !== allSelected) {
container.Checked = allSelected;
$scope.selectItem(container);
}
});
};
$scope.killAction = function() {
batch($scope.containers, Container.kill, "Killed");
};
$scope.pauseAction = function() {
batch($scope.containers, Container.pause, "Paused");
};
$scope.unpauseAction = function() {
batch($scope.containers, Container.unpause, "Unpaused");
};
$scope.removeAction = function() {
batch($scope.containers, Container.remove, "Removed");
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.toggleGetAll = function () {
Settings.displayAll = $scope.state.displayAll;
update({all: Settings.displayAll ? 1 : 0});
};
$scope.startAction = function () {
batch($scope.containers, Container.start, "Started");
};
$scope.stopAction = function () {
batch($scope.containers, Container.stop, "Stopped");
};
$scope.restartAction = function () {
batch($scope.containers, Container.restart, "Restarted");
};
$scope.killAction = function () {
batch($scope.containers, Container.kill, "Killed");
};
$scope.pauseAction = function () {
batch($scope.containers, Container.pause, "Paused");
};
$scope.unpauseAction = function () {
batch($scope.containers, Container.unpause, "Unpaused");
};
$scope.removeAction = function () {
batch($scope.containers, Container.remove, "Removed");
};
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;
}
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
});
} else {
update({all: Settings.displayAll ? 1 : 0});
}
});
}]);
@@ -0,0 +1,265 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Messages) {
$scope.formValues = {
alwaysPull: true,
Console: 'none',
Volumes: [],
Registry: '',
NetworkContainer: '',
Labels: []
};
$scope.imageConfig = {};
$scope.config = {
Image: '',
Env: [],
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: [],
Binds: [],
NetworkMode: 'bridge',
Privileged: false
},
Labels: {}
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
$scope.formValues.Volumes.splice(index, 1);
};
$scope.addEnvironmentVariable = function() {
$scope.config.Env.push({ name: '', value: ''});
};
$scope.removeEnvironmentVariable = function(index) {
$scope.config.Env.splice(index, 1);
};
$scope.addPortBinding = function() {
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.config.HostConfig.PortBindings.splice(index, 1);
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.splice(index, 1);
};
Config.$promise.then(function (c) {
var containersToHideLabels = c.hiddenLabels;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
});
Network.query({}, function (d) {
var networks = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Name: "bridge"});
networks.push({Name: "host"});
networks.push({Name: "none"});
}
networks.push({Name: "container"});
$scope.availableNetworks = networks;
if (!_.find(networks, {'Name': 'bridge'})) {
$scope.config.HostConfig.NetworkMode = 'nat';
}
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
Container.query({}, function (d) {
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
$scope.runningContainers = containers;
}, function(e) {
Messages.error("Failure", e, "Unable to retrieve running containers");
});
});
// TODO: centralize, already present in templatesController
function createContainer(config) {
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
Container.start({id: d.Id}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
});
}
// TODO: centralize, already present in templatesController
function pullImageAndCreateContainer(config) {
Image.create($scope.imageConfig, function (data) {
createContainer(config);
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Failure', e, 'Unable to pull image');
});
}
function prepareImageConfig(config) {
var image = config.Image;
var registry = $scope.formValues.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
function preparePortBindings(config) {
var bindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
if (portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
var binding = {};
if (portBinding.hostPort && 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;
}
function prepareConsole(config) {
var value = $scope.formValues.Console;
var openStdin = true;
var tty = true;
if (value === 'tty') {
openStdin = false;
} else if (value === 'interactive') {
tty = false;
} else if (value === 'none') {
openStdin = false;
tty = false;
}
config.OpenStdin = openStdin;
config.Tty = tty;
}
function prepareEnvironmentVariables(config) {
var env = [];
config.Env.forEach(function (v) {
if (v.name && v.value) {
env.push(v.name + "=" + v.value);
}
});
config.Env = env;
}
function prepareVolumes(config) {
var binds = [];
var volumes = {};
$scope.formValues.Volumes.forEach(function (volume) {
var name = volume.name;
var containerPath = volume.containerPath;
if (name && containerPath) {
var bind = name + ':' + containerPath;
volumes[containerPath] = {};
if (volume.readOnly) {
bind += ':ro';
}
binds.push(bind);
}
});
config.HostConfig.Binds = binds;
config.Volumes = volumes;
}
function prepareNetworkConfig(config) {
var mode = config.HostConfig.NetworkMode;
var container = $scope.formValues.NetworkContainer;
var containerName = container;
if (container && typeof container === 'object') {
containerName = $filter('trimcontainername')(container.Names[0]);
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
containerName = $filter('swarmcontainername')(container);
}
}
var networkMode = mode;
if (containerName) {
networkMode += ':' + containerName;
}
config.HostConfig.NetworkMode = networkMode;
}
function prepareLabels(config) {
var labels = {};
$scope.formValues.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareNetworkConfig(config);
prepareImageConfig(config);
preparePortBindings(config);
prepareConsole(config);
prepareEnvironmentVariables(config);
prepareVolumes(config);
prepareLabels(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
$('#createContainerSpinner').show();
if ($scope.formValues.alwaysPull) {
pullImageAndCreateContainer(config);
} else {
createContainer(config);
}
};
}]);
@@ -0,0 +1,376 @@
<rd-header>
<rd-header-title title="Create container"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > Add container
</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="container_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="container_name" placeholder="e.g. myContainer">
</div>
</div>
<!-- !name-input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-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>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="formValues.alwaysPull"> Always pull image before creating
</label>
</div>
</div>
</div>
<!-- !image-and-registry-inputs -->
<!-- restart-policy -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Restart policy</label>
<div class="col-sm-11">
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="no">
Never
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="always">
Always
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
<span class="radio-value">On failure</span>
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="unless-stopped">
<span class="radio-value">Unless stopped</span>
</label>
</div>
</div>
<!-- !restart-policy -->
<!-- port-mapping -->
<div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select class="form-control" ng-model="portBinding.protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<ul class="nav nav-tabs">
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
<div class="tab-pane active" id="command">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input -->
<div class="form-group">
<label for="container_command" class="col-sm-1 control-label text-left">Command</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div>
</div>
<!-- !command-input -->
<!-- entrypoint-input -->
<div class="form-group">
<label for="container_entrypoint" class="col-sm-1 control-label text-left">Entry Point</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c">
</div>
</div>
<!-- !entrypoint-input -->
<!-- workdir-user-input -->
<div class="form-group">
<label for="container_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp">
</div>
<label for="container_user" class="col-sm-1 control-label text-left">User</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="config.User" id="container_user" placeholder="e.g. nginx">
</div>
</div>
<!-- !workdir-user-input -->
<!-- console -->
<div class="form-group">
<label for="container_console" class="col-sm-1 control-label text-left">Console</label>
<div class="col-sm-11">
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="both">
Interactive & TTY <span class="small text-muted">(-i -t)</span>
</label>
</div>
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="interactive">
Interactive <span class="small text-muted">(-i)</span>
</label>
</div>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty">
TTY <span class="small text-muted">(-t)</span>
</label>
</div>
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="none">
None
</label>
</div>
</div>
</div>
<!-- !console -->
<!-- environment-variables -->
<div class="form-group">
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" 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="variable.name" placeholder="e.g. FOO">
</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="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-command -->
<!-- tab-volume -->
<div class="tab-pane" id="volumes">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes -->
<div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</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="form-control" ng-model="volume.name" ng-if="!volume.isPath">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</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>
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !volumes-input-list -->
</div>
</form>
<!-- !volumes -->
</div>
<!-- !tab-volume -->
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE'">
<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>
<div class="col-sm-9">
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
</div>
<!-- !network-input -->
<!-- container-name-input -->
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
<div class="col-sm-9">
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
</div>
</div>
<!-- !container-name-input -->
<!-- hostname-input -->
<div class="form-group">
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01">
</div>
</div>
<!-- !hostname-input -->
<!-- domainname-input -->
<div class="form-group">
<label for="container_domainname" class="col-sm-1 control-label text-left">Domain Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com">
</div>
</div>
<!-- !domainname -->
</form>
</div>
<!-- !tab-network -->
<!-- tab-labels -->
<div class="tab-pane" id="labels">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- labels -->
<div class="form-group">
<label for="container_labels" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" 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="label.name" placeholder="e.g. com.example.foo">
</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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-security -->
<div class="tab-pane" id="security">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- privileged-mode -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.HostConfig.Privileged"> Privileged mode
</label>
</div>
</div>
</div>
<!-- !privileged-mode -->
</form>
</div>
<!-- !tab-security -->
</div>
</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="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>
</div>
@@ -0,0 +1,98 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: '',
Labels: []
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false,
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
IPAM: {
Driver: 'default',
Config: []
},
Labels: {}
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.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 prepareLabelsConfig(config) {
var labels = {};
$scope.formValues.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
prepareLabelsConfig(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -0,0 +1,124 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > 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 interactive" 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 -->
<!-- labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" 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="label.name" placeholder="e.g. com.example.foo">
</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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
</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-disabled="!config.Name" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>
</div>
@@ -0,0 +1,238 @@
angular.module('createService', [])
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Messages',
function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
$scope.formValues = {
Name: '',
Image: '',
Registry: '',
Mode: 'replicated',
Replicas: 1,
Command: '',
EntryPoint: '',
WorkingDir: '',
User: '',
Env: [],
Labels: [],
ContainerLabels: [],
Volumes: [],
Network: '',
ExtraNetworks: [],
Ports: [],
Parallelism: 1,
UpdateDelay: 0,
FailureAction: 'pause'
};
$scope.addPortBinding = function() {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.formValues.Ports.splice(index, 1);
};
$scope.addExtraNetwork = function() {
$scope.formValues.ExtraNetworks.push({ Name: '' });
};
$scope.removeExtraNetwork = function(index) {
$scope.formValues.ExtraNetworks.splice(index, 1);
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
$scope.formValues.Volumes.splice(index, 1);
};
$scope.addEnvironmentVariable = function() {
$scope.formValues.Env.push({ name: '', value: ''});
};
$scope.removeEnvironmentVariable = function(index) {
$scope.formValues.Env.splice(index, 1);
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.splice(index, 1);
};
$scope.addContainerLabel = function() {
$scope.formValues.ContainerLabels.push({ name: '', value: ''});
};
$scope.removeContainerLabel = function(index) {
$scope.formValues.ContainerLabels.splice(index, 1);
};
function prepareImageConfig(config, input) {
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry);
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
}
function preparePortsConfig(config, input) {
var ports = [];
input.Ports.forEach(function (binding) {
if (binding.PublishedPort && binding.TargetPort) {
ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol });
}
});
config.EndpointSpec.Ports = ports;
}
function prepareSchedulingConfig(config, input) {
if (input.Mode === 'replicated') {
config.Mode.Replicated = {
Replicas: input.Replicas
};
} else {
config.Mode.Global = {};
}
}
function commandToArray(cmd) {
var tokens = [].concat.apply([], cmd.split('"').map(function(v,i) {
return i%2 ? v : v.split(' ');
})).filter(Boolean);
return tokens;
}
function prepareCommandConfig(config, input) {
if (input.EntryPoint) {
config.TaskTemplate.ContainerSpec.Command = commandToArray(input.EntryPoint);
}
if (input.Command) {
config.TaskTemplate.ContainerSpec.Args = commandToArray(input.Command);
}
if (input.User) {
config.TaskTemplate.ContainerSpec.User = input.User;
}
if (input.WorkingDir) {
config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir;
}
}
function prepareEnvConfig(config, input) {
var env = [];
input.Env.forEach(function (v) {
if (v.name && v.value) {
env.push(v.name + "=" + v.value);
}
});
config.TaskTemplate.ContainerSpec.Env = env;
}
function prepareLabelsConfig(config, input) {
var labels = {};
input.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
var containerLabels = {};
input.ContainerLabels.forEach(function (label) {
if (label.name && label.value) {
containerLabels[label.name] = label.value;
}
});
config.TaskTemplate.ContainerSpec.Labels = containerLabels;
}
function prepareVolumes(config, input) {
input.Volumes.forEach(function (volume) {
if (volume.Source && volume.Target) {
var mount = {};
mount.Type = volume.Bind ? 'bind' : 'volume';
mount.ReadOnly = volume.ReadOnly ? true : false;
mount.Source = volume.Source;
mount.Target = volume.Target;
config.TaskTemplate.ContainerSpec.Mounts.push(mount);
}
});
}
function prepareNetworks(config, input) {
var networks = [];
if (input.Network) {
networks.push({ Target: input.Network });
}
input.ExtraNetworks.forEach(function (network) {
networks.push({ Target: network.Name });
});
config.Networks = _.uniqWith(networks, _.isEqual);
}
function prepareUpdateConfig(config, input) {
config.UpdateConfig = {
Parallelism: input.Parallelism || 0,
Delay: input.UpdateDelay || 0,
FailureAction: input.FailureAction
};
}
function prepareConfiguration() {
var input = $scope.formValues;
var config = {
Name: input.Name,
TaskTemplate: {
ContainerSpec: {
Mounts: []
}
},
Mode: {},
EndpointSpec: {}
};
prepareSchedulingConfig(config, input);
prepareImageConfig(config, input);
preparePortsConfig(config, input);
prepareCommandConfig(config, input);
prepareEnvConfig(config, input);
prepareLabelsConfig(config, input);
prepareVolumes(config, input);
prepareNetworks(config, input);
prepareUpdateConfig(config, input);
return config;
}
function createNewService(config) {
Service.create(config, function (d) {
$('#createServiceSpinner').hide();
Messages.send('Service created', d.ID);
$state.go('services', {}, {reload: true});
}, function (e) {
$('#createServiceSpinner').hide();
Messages.error("Failure", e, 'Unable to create service');
});
}
$scope.create = function createService() {
$('#createServiceSpinner').show();
var config = prepareConfiguration();
createNewService(config);
};
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
});
Network.query({}, function (d) {
$scope.availableNetworks = d.filter(function (network) {
if (network.Scope === 'swarm') {
return network;
}
});
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
}]);
@@ -0,0 +1,395 @@
<rd-header>
<rd-header-title title="Create service"></rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > Add service
</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="service_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService">
</div>
</div>
<!-- !name-input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
</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>
</div>
<!-- !image-and-registry-inputs -->
<!-- scheduling-mode -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Scheduling mode</label>
<div class="col-sm-11">
<label class="radio-inline">
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="global">
Global
</label>
<label class="radio-inline">
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="replicated">
Replicated
</label>
</div>
</div>
<div class="form-group" ng-if="formValues.Mode === 'replicated'">
<label for="replicas" class="col-sm-1 control-label text-left">Replicas</label>
<div class="col-sm-1">
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3">
</div>
<div class="col-sm-10"></div>
</div>
<!-- !scheduling-mode -->
<!-- port-mapping -->
<div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select class="form-control" ng-model="portBinding.Protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<ul class="nav nav-tabs">
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
<div class="tab-pane active" id="command">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input -->
<div class="form-group">
<label for="service_command" class="col-sm-1 control-label text-left">Command</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div>
</div>
<!-- !command-input -->
<!-- entrypoint-input -->
<div class="form-group">
<label for="service_entrypoint" class="col-sm-1 control-label text-left">Entrypoint</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="formValues.EntryPoint" id="service_entrypoint" placeholder="e.g. /bin/sh -c">
</div>
</div>
<!-- !entrypoint-input -->
<!-- workdir-user-input -->
<div class="form-group">
<label for="service_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
</div>
<label for="service_user" class="col-sm-1 control-label text-left">User</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.User" id="service_user" placeholder="e.g. nginx">
</div>
</div>
<!-- !workdir-user-input -->
<!-- environment-variables -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.Env" 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="variable.name" placeholder="e.g. FOO">
</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="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-command -->
<!-- tab-volume -->
<div class="tab-pane" id="volumes">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes -->
<div class="form-group">
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</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.Bind">bind</span>
<select class="form-control" ng-model="volume.Source" ng-if="!volume.Bind">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select>
<input ng-if="volume.Bind" type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !volumes-input-list -->
</div>
<!-- !volumes -->
</form>
</div>
<!-- !tab-volume -->
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
<div class="col-sm-9">
<select class="form-control" ng-model="formValues.Network">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-2"></div>
</div>
<!-- !network-input -->
<!-- extra-networks -->
<div class="form-group">
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addExtraNetwork()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> network
</span>
</div>
<!-- network-input-list -->
<div style="margin-top: 10px;">
<div class="col-sm-12" ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 5px;">
<div class="input-group col-sm-9 input-group-sm col-sm-offset-1">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeExtraNetwork($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
<select class="form-control" ng-model="network.Name">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-2"></div>
</div>
</div>
<!-- !network-input-list -->
</div>
<!-- !extra-networks -->
</form>
</div>
<!-- !tab-network -->
<!-- tab-labels -->
<div class="tab-pane" id="labels">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" 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="label.name" placeholder="e.g. com.example.foo">
</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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
<!-- container-labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Container labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addContainerLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</span>
</div>
<!-- container-labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.ContainerLabels" 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="label.name" placeholder="e.g. com.example.foo">
</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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !container-labels-input-list -->
</div>
<!-- !container-labels-->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-update-config -->
<div class="tab-pane" id="update-config">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- parallelism-input -->
<div class="form-group">
<label for="parallelism" class="col-sm-1 control-label text-left">Parallelism</label>
<div class="col-sm-1">
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
</div>
<div class="col-sm-10">
<p class="small text-muted" style="margin-top: 10px;">
Maximum number of tasks to be updated simultaneously (0 to update all at once).
</p>
</div>
</div>
<!-- !parallelism-input -->
<!-- delay-input -->
<div class="form-group">
<label for="update-delay" class="col-sm-1 control-label text-left">Delay</label>
<div class="col-sm-2">
<input type="number" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 10">
</div>
<div class="col-sm-9">
<p class="small text-muted" style="margin-top: 10px;">
Amount of time between updates.
</p>
</div>
</div>
<!-- !delay-input -->
<!-- failureAction-input -->
<div class="form-group">
<label for="failure_action" class="col-sm-1 control-label text-left">Failure Action</label>
<div class="col-sm-3">
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="continue">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="pause">
Pause
</label>
</div>
<div class="col-sm-8"></div>
</div>
<!-- !failureAction-input -->
</form>
</div>
<!-- !tab-update-config -->
<!-- tab-security -->
<div class="tab-pane" id="security">
</div>
<!-- !tab-security -->
</div>
</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="createServiceSpinner" 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="services">Cancel</a>
</div>
</div>
@@ -0,0 +1,56 @@
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);
};
}]);
@@ -0,0 +1,72 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > 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 interactive" 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>
+151 -47
View File
@@ -1,49 +1,153 @@
<div class="col-xs-offset-1">
<!--<div class="sidebar span4">
<div ng-include="template" ng-controller="SideBarController"></div>
</div>-->
<div class="row">
<div class="col-xs-10" id="masthead" style="display:none">
<div class="jumbotron">
<h1>DockerUI</h1>
<p class="lead">The Linux container engine</p>
<a class="btn btn-large btn-success" href="http://docker.io">Learn more.</a>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-10">
<div class="col-xs-5">
<h3>Running Containers</h3>
<ul>
<li ng-repeat="container in containers|orderBy:predicate">
<a href="#/containers/{{ container.Id }}/">{{ container|containername }}</a>
<span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span>
</li>
</ul>
</div>
<div class="col-xs-5 text-right">
<h3>Status</h3>
<canvas id="containers-chart" class="pull-right">
Get a better browser... Your holding everyone back.
</canvas>
<div id="chart-legend"></div>
</div>
</div>
</div>
<rd-header>
<rd-header-title title="Home">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row">
<div class="col-xs-10" id="stats">
<h4>Containers created</h4>
<canvas id="containers-started-chart" width="700">
Get a better browser... You're holding everyone back.
</canvas>
<h4>Images created</h4>
<canvas id="images-created-chart" width="700">
Get a better browser... You're holding everyone back.
</canvas>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>{{ infoData.Name }}</td>
</tr>
<tr>
<td>Docker version</td>
<td>{{ infoData.ServerVersion }}</td>
</tr>
<tr>
<td>CPU</td>
<td>{{ infoData.NCPU }}</td>
</tr>
<tr>
<td>Memory</td>
<td>{{ infoData.MemTotal|humansize }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td>{{ infoData.SystemStatus[0][1] == 'primary' ? infoData.SystemStatus[3][1] : infoData.SystemStatus[4][1] }}</td>
</tr>
<tr>
<td>Swarm version</td>
<td>{{ infoData.ServerVersion|swarmversion }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td>{{ infoData.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td>{{ infoData.MemTotal|humansize: 2 }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td colspan="2"><span class="small text-muted">This node is part of a Swarm cluster</span></td>
</tr>
<tr >
<td>Node role</td>
<td>{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}</td>
</tr>
<tr ng-if="infoData.Swarm.ControlAvailable">
<td>Nodes in the cluster</td>
<td>{{ infoData.Swarm.Nodes }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="containers">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-server"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-heartbeat space-right green-icon"></i>{{ containerData.running }} running</div>
<div><i class="fa fa-heartbeat space-right red-icon"></i>{{ containerData.stopped }} stopped</div>
</div>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="images">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-clone"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-pie-chart space-right"></i>{{ imageData.size|humansize }}</div>
</div>
<div class="title">{{ imageData.total }}</div>
<div class="comment">Images</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="volumes">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-cubes"></i>
</div>
<div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
</div>
<div class="title">{{ volumeData.total }}</div>
<div class="comment">Volumes</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="networks">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-sitemap"></i>
</div>
<div class="title">{{ networkData.total }}</div>
<div class="comment">Networks</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
</div>
<div class="row">
</div>
+82 -62
View File
@@ -1,72 +1,92 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function($scope, Container, Image, Settings, LineChart) {
$scope.predicate = '-Created';
$scope.containers = [];
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Messages',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Messages) {
var getStarted = function(data) {
$scope.totalContainers = data.length;
LineChart.build('#containers-started-chart', data, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
var s = $scope;
Image.query({}, function(d) {
s.totalImages = d.length;
LineChart.build('#images-created-chart', d, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
});
};
$scope.containerData = {
total: 0
};
$scope.imageData = {
total: 0
};
$scope.networkData = {
total: 0
};
$scope.volumeData = {
total: 0
};
var opts = {animation:false};
if (Settings.firstLoad) {
$('#stats').hide();
opts.animation = true;
Settings.firstLoad = false;
$('#masthead').show();
function prepareContainerData(d, containersToHideLabels) {
var running = 0;
var stopped = 0;
setTimeout(function() {
$('#masthead').slideUp('slow');
$('#stats').slideDown('slow');
}, 5000);
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
Container.query({all: 1}, function(d) {
var running = 0
var ghost = 0;
var stopped = 0;
for (var i = 0; i < d.length; i++) {
var item = d[i];
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
if (item.Status.indexOf('Up') !== -1) {
running += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.total = containers.length;
}
if (item.Status === "Ghost") {
ghost += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
$scope.containers.push(new ContainerViewModel(item));
}
}
function prepareImageData(d) {
var images = d;
var totalImageSize = 0;
for (var i = 0; i < images.length; i++) {
var item = images[i];
totalImageSize += item.VirtualSize;
}
$scope.imageData.total = images.length;
$scope.imageData.size = totalImageSize;
}
getStarted(d);
function prepareVolumeData(d) {
var volumes = d.Volumes;
if (volumes) {
$scope.volumeData.total = volumes.length;
}
}
var c = new Chart($('#containers-chart').get(0).getContext("2d"));
var data = [
{
value: running,
color: '#5bb75b',
title: 'Running'
}, // running
{
value: stopped,
color: '#C7604C',
title: 'Stopped'
}, // stopped
{
value: ghost,
color: '#E2EAE9',
title: 'Ghost'
} // ghost
];
c.Doughnut(data, opts);
var lgd = $('#chart-legend').get(0);
legend(lgd, data);
});
function prepareNetworkData(d) {
var networks = d;
$scope.networkData.total = networks.length;
}
function prepareInfoData(d) {
var info = d;
$scope.infoData = info;
}
function fetchDashboardData(containersToHideLabels) {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
Image.query({}).$promise,
Volume.query({}).$promise,
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0], containersToHideLabels);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
prepareInfoData(d[4]);
$('#loadingViewSpinner').hide();
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to load dashboard data");
});
}
Config.$promise.then(function (c) {
fetchDashboardData(c.hiddenLabels);
});
}]);
+119
View File
@@ -0,0 +1,119 @@
<rd-header>
<rd-header-title title="Engine overview">
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>Docker</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Version</td>
<td>{{ version.Version }}</td>
</tr>
<tr>
<td>API version</td>
<td>{{ version.ApiVersion }}</td>
</tr>
<tr>
<td>Go version</td>
<td>{{ version.GoVersion }}</td>
</tr>
<tr>
<td>OS type</td>
<td>{{ version.Os }}</td>
</tr>
<tr>
<td>OS</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Architecture</td>
<td>{{ version.Arch }}</td>
</tr>
<tr>
<td>Kernel version</td>
<td>{{ version.KernelVersion }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Total CPU</td>
<td>{{ info.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr>
<td>Docker root directory</td>
<td>{{ info.DockerRootDir }}</td>
</tr>
<tr>
<td>Storage driver</td>
<td>{{ info.Driver }}</td>
</tr>
<tr>
<td>Logging driver</td>
<td>{{ info.LoggingDriver }}</td>
</tr>
<tr ng-if="info.CgroupDriver">
<td>Cgroup driver</td>
<td>{{ info.CgroupDriver }}</td>
</tr>
<tr ng-if="info.ExecutionDriver">
<td>Execution driver</td>
<td>{{ info.ExecutionDriver }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="state.loaded && info.Plugins">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-if="info.Plugins.Volume">
<td>Volume</td>
<td>{{ info.Plugins.Volume|arraytostr: ', '}}</td>
</tr>
<tr ng-if="info.Plugins.Network">
<td>Network</td>
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
</tr>
<tr ng-if="info.Plugins.Authorization">
<td>Authorization</td>
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
+24
View File
@@ -0,0 +1,24 @@
angular.module('docker', [])
.controller('DockerController', ['$scope', 'Info', 'Version', 'Messages',
function ($scope, Info, Version, Messages) {
$scope.state = {
loaded: false
};
$scope.info = {};
$scope.version = {};
Info.get({}, function (infoData) {
$scope.info = infoData;
Version.get({}, function (versionData) {
$scope.version = versionData;
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine details');
$('#loadingViewSpinner').hide();
});
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine information');
$('#loadingViewSpinner').hide();
});
}]);
+99
View File
@@ -0,0 +1,99 @@
<rd-header>
<rd-header-title title="Endpoint details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
</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="container_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="container_name" ng-model="endpoint.Name" placeholder="e.g. docker-prod01">
</div>
</div>
<!-- !name-input -->
<!-- endpoint-url-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
<div class="col-sm-10">
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div>
</div>
<!-- !endpoint-url-input -->
<!-- tls-checkbox -->
<div class="form-group" ng-if="endpointType === 'remote'">
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
<div class="col-sm-10">
<input type="checkbox" name="tls" ng-model="endpoint.TLS">
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-certs -->
<div ng-if="endpoint.TLS">
<!-- ca-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !ca-input -->
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !cert-input -->
<!-- key-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS key</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !key-input -->
</div>
<!-- !tls-certs -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
@@ -0,0 +1,56 @@
angular.module('endpoint', [])
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
$scope.state = {
error: '',
uploadInProgress: false
};
$scope.formValues = {
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
$scope.updateEndpoint = function() {
var ID = $scope.endpoint.Id;
var name = $scope.endpoint.Name;
var URL = $scope.endpoint.URL;
var TLS = $scope.endpoint.TLS;
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
var type = $scope.endpointType;
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) {
Messages.send("Endpoint updated", $scope.endpoint.Name);
$state.go('endpoints');
}, function error(err) {
$scope.state.error = err.msg;
}, function update(evt) {
if (evt.upload) {
$scope.state.uploadInProgress = evt.upload;
}
});
};
function getEndpoint(endpointID) {
$('#loadingViewSpinner').show();
EndpointService.endpoint($stateParams.id).then(function success(data) {
$('#loadingViewSpinner').hide();
$scope.endpoint = data;
if (data.URL.indexOf("unix://") === 0) {
$scope.endpointType = 'local';
} else {
$scope.endpointType = 'remote';
}
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
$scope.formValues.TLSCACert = data.TLSCACert;
$scope.formValues.TLSCert = data.TLSCert;
$scope.formValues.TLSKey = data.TLSKey;
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve endpoint details");
});
}
getEndpoint($stateParams.id);
}]);
@@ -0,0 +1,145 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2">
<!-- simple box logo -->
<div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
</div>
<!-- !simple box logo -->
<!-- init-endpoint panel -->
<div class="panel panel-default">
<div class="panel-body">
<!-- init-endpoint form -->
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
<!-- comment -->
<div class="form-group">
<p>Connect Portainer to a Docker engine or Swarm cluster endpoint.</p>
</div>
<!-- !comment input -->
<!-- endpoin-type radio -->
<div class="form-group">
<div class="radio">
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="cleanError()">Manage the Docker instance where Portainer is running</label>
</div>
<div class="radio">
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="cleanError()">Manage a remote Docker instance</label>
</div>
</div>
<!-- endpoint-type radio -->
<!-- local-endpoint -->
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
<div class="form-group">
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
<span class="small text-primary">This feature is not available with Docker <b>on</b> Windows yet.</span>
<div class="small text-primary">On Linux / Mac, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
</div>
<!-- connect button -->
<div class="form-group" style="margin-top: 10px;">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
</div>
<!-- !local-endpoint -->
<!-- remote-endpoint -->
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-3 control-label text-left">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
</div>
</div>
<!-- !name-input -->
<!-- endpoint-url-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-3 control-label text-left">Endpoint URL</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div>
</div>
<!-- !endpoint-url-input -->
<!-- tls-checkbox -->
<div class="form-group">
<label for="tls" class="col-sm-3 control-label text-left">TLS</label>
<div class="col-sm-9">
<input type="checkbox" name="tls" ng-model="formValues.TLS">
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-certs -->
<div ng-if="formValues.TLS">
<!-- ca-input -->
<div class="form-group">
<label class="col-sm-3 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCACert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !ca-input -->
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 control-label text-left">TLS certificate</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !cert-input -->
<!-- key-input -->
<div class="form-group">
<label class="col-sm-3 control-label text-left">TLS key</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSKey.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !key-input -->
</div>
<!-- !tls-certs -->
<!-- connect button -->
<div class="form-group" style="margin-top: 10px;">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
</div>
<!-- !remote-endpoint -->
</form>
<!-- !init-endpoint form -->
</div>
</div>
<!-- !init-endpoint panel -->
</div>
</div>
<!-- !simple box -->
</div>
@@ -0,0 +1,80 @@
angular.module('endpointInit', [])
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, EndpointService, StateManager, Messages) {
$scope.state = {
error: '',
uploadInProgress: false
};
$scope.formValues = {
endpointType: "remote",
Name: '',
URL: '',
TLS: false,
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
if (!_.isEmpty($scope.applicationState.endpoint)) {
$state.go('dashboard');
}
$scope.cleanError = function() {
$scope.state.error = '';
};
$scope.createLocalEndpoint = function() {
$('#initEndpointSpinner').show();
$scope.state.error = '';
var name = "local";
var URL = "unix:///var/run/docker.sock";
var TLS = false;
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
EndpointService.deleteEndpoint(0)
.then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
}, function error(err) {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to create endpoint';
});
};
$scope.createRemoteEndpoint = function() {
$('#initEndpointSpinner').show();
$scope.state.error = '';
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
var TLS = $scope.formValues.TLS;
var TLSCAFile = $scope.formValues.TLSCACert;
var TLSCertFile = $scope.formValues.TLSCert;
var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
.then(function success(data) {
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
EndpointService.deleteEndpoint(0)
.then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
}, function error(err) {
$('#initEndpointSpinner').hide();
$scope.state.uploadInProgress = false;
$scope.state.error = err.msg;
}, function update(evt) {
if (evt.upload) {
$scope.state.uploadInProgress = evt.upload;
}
});
};
}]);
+183
View File
@@ -0,0 +1,183 @@
<rd-header>
<rd-header-title title="Endpoints">
<a data-toggle="tooltip" title="Refresh" ui-sref="endpoints" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadEndpointsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Endpoint management</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 new endpoint">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
</div>
</div>
<!-- !name-input -->
<!-- endpoint-url-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div>
</div>
<!-- !endpoint-url-input -->
<!-- tls-checkbox -->
<div class="form-group">
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
<div class="col-sm-10">
<input type="checkbox" name="tls" ng-model="formValues.TLS">
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-certs -->
<div ng-if="formValues.TLS">
<!-- ca-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCACert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !ca-input -->
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !cert-input -->
<!-- key-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS key</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSKey.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !key-input -->
</div>
<!-- !tls-certs -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Endpoints">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</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"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>
<a ui-sref="endpoints" ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoints" ng-click="order('URL')">
URL
<span ng-show="sortType == 'URL' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoints" ng-click="order('TLS')">
TLS
<span ng-show="sortType == 'TLS' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
<td>{{ endpoint.URL | stripprotocol }}</td>
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td>
<td>
<span ng-if="endpoint.Id !== activeEndpoint.Id">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>
<span class="small text-muted" ng-if="endpoint.Id === activeEndpoint.Id">
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
</span>
</td>
</tr>
<tr ng-if="!endpoints">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="endpoints.length == 0">
<td colspan="5" class="text-center text-muted">No endpoints available.</td>
</tr>
</tbody>
</table>
<div ng-if="endpoints" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
@@ -0,0 +1,104 @@
angular.module('endpoints', [])
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination',
function ($scope, $state, EndpointService, Messages, Pagination) {
$scope.state = {
error: '',
uploadInProgress: false,
selectedItemCount: 0,
pagination_count: Pagination.getPaginationCount('endpoints')
};
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.formValues = {
Name: '',
URL: '',
TLS: false,
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.addEndpoint = function() {
$scope.state.error = '';
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
var TLS = $scope.formValues.TLS;
var TLSCAFile = $scope.formValues.TLSCACert;
var TLSCertFile = $scope.formValues.TLSCert;
var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) {
Messages.send("Endpoint created", name);
$state.reload();
}, function error(err) {
$scope.state.uploadInProgress = false;
$scope.state.error = err.msg;
}, function update(evt) {
if (evt.upload) {
$scope.state.uploadInProgress = evt.upload;
}
});
};
$scope.removeAction = function () {
$('#loadEndpointsSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadEndpointsSpinner').hide();
}
};
angular.forEach($scope.endpoints, function (endpoint) {
if (endpoint.Checked) {
counter = counter + 1;
EndpointService.deleteEndpoint(endpoint.Id).then(function success(data) {
Messages.send("Endpoint deleted", endpoint.Name);
var index = $scope.endpoints.indexOf(endpoint);
$scope.endpoints.splice(index, 1);
complete();
}, function error(err) {
Messages.error("Failure", err, 'Unable to remove endpoint');
complete();
});
}
});
};
function fetchEndpoints() {
$('#loadEndpointsSpinner').show();
EndpointService.endpoints().then(function success(data) {
$scope.endpoints = data;
EndpointService.getActive().then(function success(data) {
$scope.activeEndpoint = data;
$('#loadEndpointsSpinner').hide();
}, function error(err) {
$('#loadEndpointsSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve active endpoint");
});
}, function error(err) {
$('#loadEndpointsSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve endpoints");
$scope.endpoints = [];
});
}
fetchEndpoints();
}]);
+74
View File
@@ -0,0 +1,74 @@
<rd-header>
<rd-header-title title="Event list">
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadEventsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Events</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-history" title="Events">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ui-sref="events" ng-click="order('Time')">
Date
<span ng-show="sortType == 'Time' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Time' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="events" ng-click="order('Type')">
Category
<span ng-show="sortType == 'Type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="events" ng-click="order('Details')">
Details
<span ng-show="sortType == 'Details' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Details' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td>{{ event.Time|getisodatefromtimestamp }}</td>
<td>{{ event.Type }}</td>
<td>{{ event.Details }}</td>
</tr>
</tbody>
</table>
<div ng-if="events" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
+32
View File
@@ -0,0 +1,32 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination',
function ($scope, Messages, Events, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('events');
$scope.sortType = 'Time';
$scope.sortReverse = true;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('events', $scope.state.pagination_count);
};
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
Events.query({since: from, until: to},
function(d) {
$scope.events = d.map(function (item) {
return new EventViewModel(item);
});
$('#loadEventsSpinner').hide();
},
function (e) {
$('#loadEventsSpinner').hide();
Messages.error("Failure", e, "Unable to load events");
});
}]);
@@ -1,7 +0,0 @@
angular.module('footer', [])
.controller('FooterController', ['$scope', 'Settings', function($scope, Settings) {
$scope.template = 'app/components/footer/statusbar.html';
$scope.uiVersion = Settings.uiVersion;
$scope.apiVersion = Settings.version;
}]);
-3
View File
@@ -1,3 +0,0 @@
<footer class="center well">
<p><small>Docker API Version: <strong>{{ apiVersion }}</strong> UI Version: <strong>{{ uiVersion }}</strong> <a class="pull-right" href="https://github.com/crosbymichael/dockerui">dockerui</a></small></p>
</footer>
+157 -102
View File
@@ -1,107 +1,162 @@
<div ng-include="template" ng-controller="StartContainerController"></div>
<rd-header>
<rd-header-title title="Image details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
<div class="alert alert-error" id="error-message" style="display:none">
{{ error }}
<div class="row" ng-if="RepoTags.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa fa-tags" title="Image tags"></rd-widget-header>
<rd-widget-body classes="no-padding">
<div style="margin: 5px 10px;">
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag space-right">
<a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)">
<i class="fa fa-upload white-icon" aria-hidden="true"></i>
</a>
{{ tag }}
<a data-toggle="tooltip" class="interactive" title="Remove tag" ng-click="removeImage(tag)">
<i class="fa fa-trash-o white-icon" aria-hidden="true"></i>
</a>
</span>
</div>
<div style="margin: 5px 10px;">
<span class="small text-muted">
Note: you can click on the upload icon to push an image
and on the trash icon to delete a tag
</span>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="detail">
<h4>Image: {{ tag }}</h4>
<div class="btn-group detail">
<button class="btn btn-success" data-toggle="modal" data-target="#create-modal">Create</button>
</div>
<div>
<h4>Containers created:</h4>
<canvas id="containers-started-chart" width="750">
Get a better broswer... Your holding everyone back.
</canvas>
</div>
<table class="table table-striped">
<tbody>
<tr>
<td>Created:</td>
<td>{{ image.created }}</td>
</tr>
<tr>
<td>Parent:</td>
<td><a href="#/images/{{ image.parent }}/">{{ image.parent }}</a></td>
</tr>
<tr>
<td>Size:</td>
<td>{{ image.Size|humansize }}</td>
</tr>
<tr>
<td>Hostname:</td>
<td>{{ image.container_config.Hostname }}</td>
</tr>
<tr>
<td>User:</td>
<td>{{ image.container_config.User }}</td>
</tr>
<tr>
<td>Cmd:</td>
<td>{{ image.container_config.Cmd }}</td>
</tr>
<tr>
<td>Volumes:</td>
<td>{{ image.container_config.Volumes }}</td>
</tr>
<tr>
<td>Volumes from:</td>
<td>{{ image.container_config.VolumesFrom }}</td>
</tr>
<tr>
<td>Comment:</td>
<td>{{ image.comment }}</td>
</tr>
</tbody>
</table>
<div class="row-fluid">
<div class="span1">
History:
</div>
<div class="span5">
<i class="icon-refresh" style="width:32px;height:32px;" ng-click="getHistory()"></i>
</div>
</div>
<div class="well well-large">
<ul>
<li ng-repeat="change in history">
<strong>{{ change.Id }}</strong>: Created: {{ change.Created|getdate }} Created by: {{ change.CreatedBy }}
</li>
</ul>
</div>
<hr />
<div class="row-fluid">
<form class="form-inline" role="form">
<fieldset>
<legend>Tag image</legend>
<div class="form-group">
<label>Tag:</label>
<input type="text" placeholder="repo..." ng-model="tag.repo" class="form-control">
</div>
<div class="form-group">
<label class="checkbox">
<input type="checkbox" ng-model="tag.force" class="form-control"/> Force?
</label>
</div>
<input type="button" ng-click="updateTag()" value="Tag" class="btn btn-primary"/>
</fieldset>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tag" title="Tag the image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</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">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</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.Image" ng-click="tagImage()">Tag</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</div>
<hr />
<div class="btn-remove">
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="remove()">Remove Image</button>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Image details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>
{{ image.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this image</button>
</td>
</tr>
<tr ng-if="image.Parent">
<td>Parent</td>
<td><a ui-sref="image({id: image.Parent})">{{ image.Parent }}</a></td>
</tr>
<tr>
<td>Size</td>
<td>{{ image.VirtualSize|humansize }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ image.Created|getisodate }}</td>
</tr>
<tr>
<td>Build</td>
<td>Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }}</td>
</tr>
<tr ng-if="image.Author">
<td>Author</td>
<td>{{ image.Author }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Dockerfile details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CMD</td>
<td><code>{{ image.ContainerConfig.Cmd|command }}</code></td>
</tr>
<tr ng-if="image.ContainerConfig.Entrypoint">
<td>ENTRYPOINT</td>
<td><code>{{ image.ContainerConfig.Entrypoint|command }}</code></td>
</tr>
<tr ng-if="image.ContainerConfig.ExposedPorts">
<td>EXPOSE</td>
<td>
<span class="label label-default space-right" ng-repeat="port in exposedPorts">
{{ port }}
</span>
</td>
</tr>
<tr ng-if="image.ContainerConfig.Volumes">
<td>VOLUME</td>
<td>
<span class="label label-default space-right" ng-repeat="volume in volumes">
{{ volume }}
</span>
</td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in image.ContainerConfig.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
+83 -66
View File
@@ -1,72 +1,89 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$q', '$routeParams', '$location', 'Image', 'Container', 'Messages', 'LineChart',
function($scope, $q, $routeParams, $location, Image, Container, Messages, LineChart) {
$scope.history = [];
$scope.tag = {repo: '', force: false};
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.remove = function() {
Image.remove({id: $routeParams.id}, function(d) {
Messages.send("Image Removed", $routeParams.id);
}, function(e) {
$scope.error = e.data;
$('#error-message').show();
});
};
$scope.getHistory = function() {
Image.history({id: $routeParams.id}, function(d) {
$scope.history = d;
});
};
$scope.updateTag = function() {
var tag = $scope.tag;
Image.tag({id: $routeParams.id, repo: tag.repo, force: tag.force ? 1 : 0}, function(d) {
Messages.send("Tag Added", $routeParams.id);
}, function(e) {
$scope.error = e.data;
$('#error-message').show();
});
};
function getContainersFromImage($q, Container, tag) {
var defer = $q.defer();
Container.query({all:1, notruc:1}, function(d) {
var containers = [];
for (var i = 0; i < d.length; i++) {
var c = d[i];
if (c.Image == tag) {
containers.push(new ContainerViewModel(c));
}
}
defer.resolve(containers);
});
return defer.promise;
}
Image.get({id: $routeParams.id}, function(d) {
$scope.image = d;
$scope.tag = d.id;
var t = $routeParams.tag;
if (t && t !== ":") {
$scope.tag = t;
var promise = getContainersFromImage($q, Container, t);
promise.then(function(containers) {
LineChart.build('#containers-started-chart', containers, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
});
// 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) {
if (image.Id === imageId && image.RepoTags[0] !== '<none>:<none>') {
$scope.RepoTags = image.RepoTags;
}
}, function(e) {
if (e.status === 404) {
$('.detail').hide();
$scope.error = "Image not found.<br />" + $routeParams.id;
} else {
$scope.error = e.data;
}
$('#error-message').show();
});
});
}
$scope.getHistory();
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(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("Failure", e, "Unable to tag image");
});
};
$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("Failure", e, "Unable to push image");
});
};
$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("Failure", e, 'Unable to remove image');
});
};
$('#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");
});
}]);
+133 -31
View File
@@ -1,33 +1,135 @@
<rd-header>
<rd-header-title title="Image list">
<a data-toggle="tooltip" title="Refresh" ui-sref="images" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadImagesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Images</rd-header-content>
</rd-header>
<div ng-include="template" ng-controller="BuilderController"></div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-download" title="Pull image ">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<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 -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</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.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>
</rd-widget-body>
</rd-widget>
</div>
</div>
<h2>Images:</h2>
<ul class="nav nav-pills">
<li class="dropdown">
<a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" data-target="#">Actions <b class="caret"></b></a>
<ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
<li><a tabindex="-1" href="" ng-click="removeAction()">Remove</a></li>
</ul>
</li>
</ul>
<table class="table table-striped">
<thead>
<tr>
<th><input type="checkbox" ng-model="toggle" ng-change="toggleSelectAll()" /> Action</th>
<th>Id</th>
<th>Repository</th>
<th>VirtualSize</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="image in images | orderBy:predicate">
<td><input type="checkbox" ng-model="image.Checked" /></td>
<td><a href="#/images/{{ image.Id }}/?tag={{ image|repotag }}">{{ image.Id|truncate:20}}</a></td>
<td>{{ image|repotag }}</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getdate }}</td>
</tr>
</tbody>
</table>
<div class="row">
<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">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</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"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="images" 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="images" ng-click="order('RepoTags')">
Tags
<span ng-show="sortType == 'RepoTags' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'RepoTags' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('VirtualSize')">
Size
<span ng-show="sortType == 'VirtualSize' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>
<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>
<tr ng-if="!images">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="images.length == 0">
<td colspan="5" class="text-center text-muted">No images available.</td>
</tr>
</tbody>
</table>
<div ng-if="images" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
+101 -45
View File
@@ -1,52 +1,108 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', 'Image', 'ViewSpinner', 'Messages',
function($scope, Image, ViewSpinner, Messages) {
$scope.toggle = false;
$scope.predicate = '-Created';
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.showBuilder = function() {
$('#build-modal').modal('show');
};
$scope.config = {
Image: '',
Registry: ''
};
$scope.removeAction = function() {
ViewSpinner.spin();
var counter = 0;
var complete = function() {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
}
};
angular.forEach($scope.images, function(i) {
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);
complete();
}, function(e) {
Messages.error("Failure", e.data);
complete();
});
}
});
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.toggleSelectAll = function() {
angular.forEach($scope.images, function(i) {
i.Checked = $scope.toggle;
});
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('images', $scope.state.pagination_count);
};
ViewSpinner.spin();
Image.query({}, function(d) {
$scope.images = d.map(function(item) { return new ImageViewModel(item); });
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredImages, function (image) {
if (image.Checked !== allSelected) {
image.Checked = allSelected;
$scope.selectItem(image);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
Messages.error('Error', {}, detail.error);
} else {
$('#pullImageSpinner').hide();
$state.reload();
}
}, function (e) {
$('#pullImageSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
});
};
$scope.removeAction = function () {
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadImagesSpinner').hide();
}
};
angular.forEach($scope.images, function (i) {
if (i.Checked) {
counter = counter + 1;
Image.remove({id: i.Id}, function (d) {
if (d[0].message) {
$('#loadImagesSpinner').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, 'Unable to remove image');
complete();
});
}
});
};
function fetchImages() {
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
$('#loadImagesSpinner').hide();
}, function (e) {
$('#loadImagesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve images");
$scope.images = [];
});
}
Config.$promise.then(function (c) {
fetchImages();
});
}]);
+36
View File
@@ -0,0 +1,36 @@
angular.module('main', [])
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
function ($scope, $cookieStore, StateManager) {
/**
* Sidebar Toggle & Cookie Control
*/
var mobileView = 992;
$scope.getWidth = function() {
return window.innerWidth;
};
$scope.applicationState = StateManager.getState();
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) {
if (angular.isDefined($cookieStore.get('toggle'))) {
$scope.toggle = ! $cookieStore.get('toggle') ? false : true;
} else {
$scope.toggle = true;
}
} else {
$scope.toggle = false;
}
});
$scope.toggleSidebar = function() {
$scope.toggle = !$scope.toggle;
$cookieStore.put('toggle', $scope.toggle);
};
window.onresize = function() {
$scope.$apply();
};
}]);
-9
View File
@@ -1,9 +0,0 @@
<div class="masthead">
<h3 class="text-muted">DockerUI</h3>
<ul class="nav well">
<li><a href="#">Dashboard</a></li>
<li><a href="#/containers/">Containers</a></li>
<li><a href="#/images/">Images</a></li>
<li><a href="#/settings/">Settings</a></li>
</ul>
</div>
@@ -1,4 +0,0 @@
angular.module('masthead', [])
.controller('MastheadController', ['$scope', function($scope) {
$scope.template = 'app/components/masthead/masthead.html';
}]);
+98
View File
@@ -0,0 +1,98 @@
<rd-header>
<rd-header-title title="Network details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</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-sitemap" title="Network details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>{{ network.Name }}</td>
</tr>
<tr>
<td>ID</td>
<td>
{{ network.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this network</button>
</td>
</tr>
<tr>
<td>Driver</td>
<td>{{ network.Driver }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ network.Scope }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Subnet">
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Gateway">
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="!(network.Options | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cogs" title="Network options"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-repeat="(key, value) in network.Options">
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="!(network.Containers | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Containers in network"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<th>Container Name</th>
<th>IPv4 Address</th>
<th>IPv6 Address</th>
<th>MacAddress</th>
<th>Actions</th>
</thead>
<tbody>
<tr ng-repeat="container in containersInNetwork">
<td><a ui-sref="container({id: container.Id})">{{ container.Name }}</a></td>
<td>{{ container.IPv4Address || '-' }}</td>
<td>{{ container.IPv6Address || '-' }}</td>
<td>{{ container.MacAddress || '-' }}</td>
<td>
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(network, container.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
+100
View File
@@ -0,0 +1,100 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) {
$scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove network");
});
};
$scope.containerLeaveNetwork = function containerLeaveNetwork(network, containerId) {
$('#loadingViewSpinner').show();
Network.disconnect({id: $stateParams.id}, { Container: containerId, Force: false }, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Container left network", $stateParams.id);
$state.go('network', {id: network.Id}, {reload: true});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container from network");
});
};
function getNetwork() {
$('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}, function success(data) {
$scope.network = data;
getContainersInNetwork(data);
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve network info");
});
}
function filterContainersInNetwork(network, containers) {
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
}
var containersInNetwork = [];
containers.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id];
containerInNetwork.Id = container.Id;
// Name is not available in Docker 1.9
if (!containerInNetwork.Name) {
containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
}
containersInNetwork.push(containerInNetwork);
});
$scope.containersInNetwork = containersInNetwork;
}
function getContainersInNetwork(network) {
if (network.Containers) {
if ($scope.applicationState.endpoint.apiVersion < 1.24) {
Container.query({}, function success(data) {
var containersInNetwork = data.filter(function filter(container) {
if (container.HostConfig.NetworkMode === network.Name) {
return container;
}
});
filterContainersInNetwork(network, containersInNetwork);
$('#loadingViewSpinner').hide();
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve containers in network");
});
} else {
Container.query({
filters: {network: [$stateParams.id]}
}, function success(data) {
filterContainersInNetwork(network, data);
$('#loadingViewSpinner').hide();
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve containers in network");
});
}
}
}
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
getNetwork();
});
}]);
+159
View File
@@ -0,0 +1,159 @@
<rd-header>
<rd-header-title title="Network list">
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadNetworksSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<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 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" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<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>
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</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>
<button type="button" class="btn btn-default btn-sm" ui-sref="actions.create.network">Advanced settings...</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="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</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"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="networks" ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<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
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<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
<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
<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>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<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>
<tr ng-if="!networks">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="networks.length == 0">
<td colspan="8" class="text-center text-muted">No networks available.</td>
</tr>
</tbody>
</table>
<div ng-if="networks" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
@@ -0,0 +1,115 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
function ($scope, $state, Network, Config, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.config = {
Name: ''
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
};
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
config.Driver = 'overlay';
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
config.IPAM = {
Driver: 'default'
};
}
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.reload();
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function(allSelected) {
angular.forEach($scope.state.filteredNetworks, function (network) {
if (network.Checked !== allSelected) {
network.Checked = allSelected;
$scope.selectItem(network);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadNetworksSpinner').hide();
}
};
angular.forEach($scope.networks, function (network) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
} else {
Messages.send("Network removed", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove network');
complete();
});
}
});
};
function fetchNetworks() {
$('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
}, function (e) {
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
$scope.networks = [];
});
}
Config.$promise.then(function (c) {
fetchNetworks();
});
}]);
+273
View File
@@ -0,0 +1,273 @@
<rd-header>
<rd-header-title title="Node details">
<a data-toggle="tooltip" title="Refresh" ui-sref="node({id: node.Id})" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="swarm">Swarm nodes</a> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
</rd-header-content>
</rd-header>
<div class="row" ng-if="!node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div ng-if="loading">
<i class="fa fa-cog fa-spin"></i> Loading...
</div>
<rd-widget ng-if="!loading">
<rd-widget-header icon="fa-object-group" title="Node does not exist"></rd-widget-header>
<rd-widget-body>
<p>It looks like the node you wish to inspect does not exist.</p>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Node specification"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
<input type="text" class="input-sm" ng-model="node.Name" placeholder="e.g. my-manager" ng-change="updateNodeAttribute(node, 'Name')">
</td>
</tr>
<tr>
<td>Host name</td>
<td>{{ node.Hostname }}</td>
</tr>
<tr>
<td>Role</td>
<td>{{ node.Role }}</td>
</tr>
<tr>
<td>Availability</td>
<td>
<div class="input-group input-group-sm">
<select name="nodeAvailability" class="selectpicker form-control" ng-model="node.Availability" ng-change="updateNodeAttribute(node, 'Availability')">
<option value="active">Active</option>
<option value="pause">Pause</option>
<option value="drain">Drain</option>
</select>
</div>
</td>
</tr>
<tr>
<td>Status</td>
<td><span class="label label-{{ node.Status|nodestatusbadge }}">{{ node.Status }}</span></td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<p class="small text-muted">
View the Docker Swarm mode Node documentation <a href="https://docs.docker.com/engine/swarm/manage-nodes/" target="self">here</a>.
</p>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(node, ['Name', 'Availability'])" ng-click="updateNode(node)">Apply changes</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node && node.Role === 'manager'">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Manager status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Leader</td>
<td>
<span ng-if="node.Leader"><i class="fa fa-check green-icon" aria-hidden="true"></i> Yes</span>
<span ng-if="!node.Leader"><i class="fa fa-times red-icon" aria-hidden="true"></i> No</span>
</td>
</tr>
<tr>
<td>Reachability</td>
<td>{{ node.Reachability }}</td>
</tr>
<tr>
<td>Manager address</td>
<td>{{ node.ManagerAddr }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Node description"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CPU</td>
<td>{{ node.CPUs / 1000000000 }}</td>
</tr>
<tr>
<td>Memory</td>
<td>{{ node.Memory|humansize: 2 }}</td>
</tr>
<tr>
<td>Platform</td>
<td>{{ node.PlatformOS }} {{ node.PlatformArchitecture }} </td>
</tr>
<tr>
<td>Docker Engine version</td>
<td>{{ node.EngineVersion }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Node labels">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="addLabel(node)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="!node.Labels || node.Labels.length === 0">
<p>There are no labels for this node.</p>
</rd-widget-body>
<rd-widget-body classes="no-padding" ng-if="node.Labels && node.Labels.length > 0">
<table class="table">
<thead>
<tr>
<th>Label</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in node.Labels">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(node, label)">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(node, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(node, ['Labels'])" ng-click="updateNode(node)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node && tasks.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ui-sref="node" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td>{{ task.Image }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+112
View File
@@ -0,0 +1,112 @@
angular.module('node', [])
.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Messages',
function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Messages) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('node_tasks');
$scope.loading = true;
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
var originalNode = {};
var editedKeys = [];
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('node_tasks', $scope.state.pagination_count);
};
$scope.updateNodeAttribute = function updateNodeAttribute(node, key) {
editedKeys.push(key);
};
$scope.addLabel = function addLabel(node) {
node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' });
$scope.updateNodeAttribute(node, 'Labels');
};
$scope.removeLabel = function removeLabel(node, index) {
var removedElement = node.Labels.splice(index, 1);
if (removedElement !== null) {
$scope.updateNodeAttribute(node, 'Labels');
}
};
$scope.updateLabel = function updateLabel(node, label) {
if (label.value !== label.originalValue || label.key !== label.originalKey) {
$scope.updateNodeAttribute(node, 'Labels');
}
};
$scope.hasChanges = function(node, elements) {
if (!elements) {
elements = Object.keys(originalNode);
}
var hasChanges = false;
elements.forEach(function(key) {
hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]);
});
return hasChanges;
};
$scope.cancelChanges = function(node) {
editedKeys.forEach(function(key) {
node[key] = originalNode[key];
});
editedKeys = [];
};
$scope.updateNode = function updateNode(node) {
var config = NodeHelper.nodeToConfig(node.Model);
config.Name = node.Name;
config.Availability = node.Availability;
config.Role = node.Role;
config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels);
Node.update({ id: node.Id, version: node.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Node successfully updated", "Node updated");
$state.go('node', {id: node.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Failed to update node");
});
};
function loadNodeAndTasks() {
$scope.loading = true;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.get({ id: $stateParams.id}, function(d) {
if (d.message) {
Messages.error("Failure", e, "Unable to inspect the node");
} else {
var node = new NodeViewModel(d);
originalNode = angular.copy(node);
$scope.node = node;
getTasks(d);
}
$scope.loading = false;
});
} else {
$scope.loading = false;
}
}
function getTasks(node) {
if (node) {
Task.query({filters: {node: [node.ID]}}, function (tasks) {
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, [node]);
});
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve tasks associated to the node");
});
}
}
loadNodeAndTasks();
}]);
+293
View File
@@ -0,0 +1,293 @@
<rd-header>
<rd-header-title title="Service details">
<a data-toggle="tooltip" title="Refresh" ui-sref="service({id: service.Id})" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
</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-list-alt" title="Service details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!service.EditName">
{{ service.Name }}
<a href="" data-toggle="tooltip" title="Edit service name" ng-click="service.EditName = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="service.EditName">
<input type="text" class="containerNameInput" ng-model="service.newServiceName">
<a class="interactive" ng-click="service.EditName = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="renameService(service)"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr>
<td>ID</td>
<td>
{{ service.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this service</button>
</td>
</tr>
<tr>
<td>Scheduling mode</td>
<td>{{ service.Mode }}</td>
</tr>
<tr ng-if="service.Mode === 'replicated'">
<td>Replicas</td>
<td>
<span ng-if="service.Mode === 'replicated' && !service.EditReplicas">
{{ service.Replicas }}
<a class="interactive" ng-click="service.EditReplicas = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span>
<span ng-if="service.Mode === 'replicated' && service.EditReplicas">
<input class="input-sm" type="number" ng-model="service.newServiceReplicas" />
<a class="interactive" ng-click="service.EditReplicas = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Image</td>
<td ng-if="!service.EditImage">
{{ service.Image }}
<a href="" data-toggle="tooltip" title="Edit service image" ng-click="service.EditImage = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="service.EditImage">
<input type="text" class="containerNameInput" ng-model="service.newServiceImage">
<a class="interactive" ng-click="service.EditImage = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeServiceImage(service)"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr ng-if="service.Ports">
<td>Published ports</td>
<td>
<div ng-repeat="mapping in service.Ports">
{{ mapping.TargetPort }} <i class="fa fa-long-arrow-right"></i> {{ mapping.PublishedPort }}
</div>
</td>
</tr>
<tr ng-if="service.EnvironmentVariables">
<td>Environment variables</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addEnvironmentVariable(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="var in service.EnvironmentVariables" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
</td>
</tr>
<tr>
<td>Labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
<tr>
<td>Container labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addContainerLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceContainerLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
<tr>
<td>Update Parallelism</td>
<td>
<span ng-if="!service.EditParallelism">
{{ service.UpdateParallelism }}
<a class="interactive" ng-click="service.EditParallelism = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
</span>
<span ng-if="service.EditParallelism">
<input class="input-sm" type="number" ng-model="service.newServiceUpdateParallelism" />
<a class="interactive" ng-click="service.EditParallelism = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeParallelism(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Update Delay</td>
<td>
<span ng-if="!service.EditDelay">
{{ service.UpdateDelay }}
<a class="interactive" ng-click="service.EditDelay = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
</span>
<span ng-if="service.EditDelay">
<input class="input-sm" type="number" ng-model="service.newServiceUpdateDelay" />
<a class="interactive" ng-click="service.EditDelay = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeUpdateDelay(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Update Failure Action</td>
<td>
<div class="form-group">
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
Pause
</label>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer ng-if="service.hasChanges">
<div>
<button type="button" class="btn btn-primary" ng-click="updateService(service)">Save changes</button>
<button type="button" class="btn btn-default" ng-click="cancelChanges(service)">Reset</button>
</div>
</rd-widget-footer>
</rd-widget>
</div>
</div>
<div class="row" ng-if="tasks.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ui-sref="service" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="displayNode">
<a ui-sref="service" ng-click="order('Node')">
Node
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td ng-if="displayNode">{{ task.Node }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+237
View File
@@ -0,0 +1,237 @@
angular.module('service', [])
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
$scope.service = {};
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
var previousServiceValues = {};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('service_tasks', $scope.state.pagination_count);
};
$scope.renameService = function renameService(service) {
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
service.EditName = false;
};
$scope.changeServiceImage = function changeServiceImage(service) {
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
service.EditImage = false;
};
$scope.scaleService = function scaleService(service) {
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
updateServiceAttribute(service, 'Replicas', replicas);
service.EditReplicas = false;
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
service.hasChanges = true;
};
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
service.hasChanges = service.hasChanges || variable.value !== variable.originalValue;
};
$scope.addLabel = function addLabel(service) {
service.hasChanges = true;
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
};
$scope.removeLabel = function removeLabel(service, index) {
var removedElement = service.ServiceLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateLabel = function updateLabel(service, label) {
service.hasChanges = service.hasChanges || label.value !== label.originalValue;
};
$scope.addContainerLabel = function addContainerLabel(service) {
service.hasChanges = true;
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
};
$scope.removeContainerLabel = function removeContainerLabel(service, index) {
var removedElement = service.ServiceContainerLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.changeParallelism = function changeParallelism(service) {
updateServiceAttribute(service, 'UpdateParallelism', service.newServiceUpdateParallelism);
service.EditParallelism = false;
};
$scope.changeUpdateDelay = function changeUpdateDelay(service) {
updateServiceAttribute(service, 'UpdateDelay', service.newServiceUpdateDelay);
service.EditDelay = false;
};
$scope.changeUpdateFailureAction = function changeUpdateFailureAction(service) {
updateServiceAttribute(service, 'UpdateFailureAction', service.newServiceUpdateFailureAction);
};
$scope.cancelChanges = function changeServiceImage(service) {
Object.keys(previousServiceValues).forEach(function(attribute) {
service[attribute] = previousServiceValues[attribute]; // reset service values
service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields
});
previousServiceValues = {}; // clear out all changes
// clear out environment variable changes
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
service.hasChanges = false;
};
$scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.newServiceName;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.newServiceImage;
if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas;
}
config.UpdateConfig = {
Parallelism: service.newServiceUpdateParallelism,
Delay: service.newServiceUpdateDelay,
FailureAction: service.newServiceUpdateFailureAction
};
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully updated", "Service updated");
$state.go('service', {id: service.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to update service");
});
};
$scope.removeService = function removeService() {
$('#loadingViewSpinner').show();
Service.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Service removed", $stateParams.id);
$state.go('services', {});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove service");
});
};
function fetchServiceDetails() {
$('#loadingViewSpinner').show();
Service.get({id: $stateParams.id}, function (d) {
var service = new ServiceViewModel(d);
service.newServiceName = service.Name;
service.newServiceImage = service.Image;
service.newServiceReplicas = service.Replicas;
service.newServiceUpdateParallelism = service.UpdateParallelism;
service.newServiceUpdateDelay = service.UpdateDelay;
service.newServiceUpdateFailureAction = service.UpdateFailureAction;
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
$scope.service = service;
Task.query({filters: {service: [service.Name]}}, function (tasks) {
Node.query({}, function (nodes) {
$scope.displayNode = true;
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, nodes);
});
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, null);
});
Messages.error("Failure", e, "Unable to retrieve node information");
});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve tasks associated to the service");
});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve service details");
});
}
function updateServiceAttribute(service, name, newValue) {
// ensure we only capture the original previous value, in case we update the attribute multiple times
if (!previousServiceValues[name]) {
previousServiceValues[name] = service[name];
}
// update the attribute
service[name] = newValue;
service.hasChanges = true;
}
function translateEnvironmentVariables(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true});
});
return variables;
}
return [];
}
function translateEnvironmentVariablesToEnv(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
if (variable.key && variable.key !== '' && variable.value && variable.value !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
}
function translateLabelsToServiceLabels(Labels) {
var labels = [];
if (Labels) {
Object.keys(Labels).forEach(function(key) {
labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true});
});
}
return labels;
}
function translateServiceLabelsToLabels(labels) {
var Labels = {};
if (labels) {
labels.forEach(function(label) {
Labels[label.key] = label.value;
});
}
return Labels;
}
fetchServiceDetails();
}]);
+95
View File
@@ -0,0 +1,95 @@
<rd-header>
<rd-header-title title="Service list">
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadServicesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Services</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-list-alt" title="Services">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<th></th>
<th>
<a ui-sref="services" ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="services" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="services" ng-click="order('Mode')">
Scheduling mode
<span ng-show="sortType == 'Mode' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
<td>{{ service.Image }}</td>
<td>
{{ service.Mode }}
<span ng-if="service.Mode === 'replicated' && !service.Scale">
<code data-toggle="tooltip" title="Replicas">{{ service.Replicas }}</code>
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span>
<span ng-if="service.Mode === 'replicated' && service.Scale">
<input class="input-sm" type="number" ng-model="service.Replicas" />
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr ng-if="!services">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="services.length == 0">
<td colspan="4" class="text-center text-muted">No services available.</td>
</tr>
</tbody>
</table>
<div ng-if="services" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
@@ -0,0 +1,88 @@
angular.module('services', [])
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('services');
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('services', $scope.state.pagination_count);
};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.scaleService = function scaleService(service) {
$('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Mode.Replicated.Replicas = service.Replicas;
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
$state.reload();
}, function (e) {
$('#loadServicesSpinner').hide();
service.Scale = false;
service.Replicas = service.ReplicaCount;
Messages.error("Failure", e, "Unable to scale service");
});
};
$scope.removeAction = function () {
$('#loadServicesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadServicesSpinner').hide();
}
};
angular.forEach($scope.services, function (service) {
if (service.Checked) {
counter = counter + 1;
Service.remove({id: service.Id}, function (d) {
if (d.message) {
$('#loadServicesSpinner').hide();
Messages.error("Unable to remove service", {}, d[0].message);
} else {
Messages.send("Service deleted", service.Id);
var index = $scope.services.indexOf(service);
$scope.services.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove service');
complete();
});
}
});
};
function fetchServices() {
$('#loadServicesSpinner').show();
Service.query({}, function (d) {
$scope.services = d.map(function (service) {
return new ServiceViewModel(service);
});
$('#loadServicesSpinner').hide();
}, function(e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve services");
$scope.services = [];
});
}
fetchServices();
}]);
+65 -47
View File
@@ -1,49 +1,67 @@
<div class="detail">
<h2>Docker Information</h2>
<div>
<p class="lead">
<strong>Endpoint: </strong>{{ endpoint }}<br />
<strong>Api Version: </strong>{{ apiVersion }}<br />
<strong>Version: </strong>{{ docker.Version }}<br />
<strong>Git Commit: </strong>{{ docker.GitCommit }}<br />
<strong>Go Version: </strong>{{ docker.GoVersion }}<br />
</p>
</div>
<rd-header>
<rd-header-title title="Settings">
</rd-header-title>
<rd-header-content>Settings</rd-header-content>
</rd-header>
<table class="table table-striped">
<tbody>
<tr>
<td>Containers:</td>
<td>{{ info.Containers }}</td>
</tr>
<tr>
<td>Images:</td>
<td>{{ info.Images }}</td>
</tr>
<tr>
<td>Debug:</td>
<td>{{ info.Debug }}</td>
</tr>
<tr>
<td>NFd:</td>
<td>{{ info.NFd }}</td>
</tr>
<tr>
<td>NGoroutines:</td>
<td>{{ info.NGoroutines }}</td>
</tr>
<tr>
<td>MemoryLimit:</td>
<td>{{ info.MemoryLimit }}</td>
</tr>
<tr>
<td>SwapLimit:</td>
<td>{{ info.SwapLimit }}</td>
</tr>
<tr>
<td>NFd:</td>
<td>{{ info.NFd }}</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- current-password-input -->
<div class="form-group">
<label for="current_password" class="col-sm-2 control-label text-left">Current password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password">
</div>
</div>
</div>
<!-- !current-password-input -->
<div class="form-group" style="margin-left: 5px;">
<p>
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword.length >= 8]" aria-hidden="true"></i>
Your new password must be at least 8 characters long
</p>
</div>
<!-- new-password-input -->
<div class="form-group">
<label for="new_password" class="col-sm-2 control-label text-left">New password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password">
</div>
</div>
</div>
<!-- !new-password-input -->
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<div class="form-group">
<div class="col-sm-2">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
</div>
<div class="col-sm-10">
<p class="pull-left text-danger" ng-if="invalidPassword" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Current password is not valid
</p>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
+27 -8
View File
@@ -1,11 +1,30 @@
angular.module('settings', [])
.controller('SettingsController', ['$scope', 'System', 'Docker', 'Settings', 'Messages',
function($scope, System, Docker, Settings, Messages) {
$scope.info = {};
$scope.docker = {};
$scope.endpoint = Settings.endpoint;
$scope.apiVersion = Settings.version;
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages',
function ($scope, $state, $sanitize, Users, Messages) {
$scope.formValues = {
currentPassword: '',
newPassword: '',
confirmPassword: ''
};
Docker.get({}, function(d) { $scope.docker = d; });
System.get({}, function(d) { $scope.info = d; });
$scope.updatePassword = function() {
$scope.invalidPassword = false;
$scope.error = false;
var currentPassword = $sanitize($scope.formValues.currentPassword);
Users.checkPassword({ username: $scope.username, password: currentPassword }, function (d) {
if (d.valid) {
var newPassword = $sanitize($scope.formValues.newPassword);
Users.update({ username: $scope.username, password: newPassword }, function (d) {
Messages.send("Success", "Password successfully updated");
$state.reload();
}, function (e) {
Messages.error("Failure", e, "Unable to update password");
});
} else {
$scope.invalidPassword = true;
}
}, function (e) {
Messages.error("Failure", e, "Unable to check password validity");
});
};
}]);
+66 -11
View File
@@ -1,11 +1,66 @@
<div class="well">
<strong>Running containers:</strong>
<br />
<strong>Endpoint: </strong>{{ endpoint }}
<ul>
<li ng-repeat="container in containers">
<a href="#/containers/{{ container.Id }}/">{{ container.Id|truncate:20 }}</a>
<span class="pull-right label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span>
</li>
</ul>
</div>
<!-- Sidebar -->
<div id="sidebar-wrapper">
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()" class="interactive">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
<li class="sidebar-title">
<span>Active endpoint</span>
</li>
<li class="sidebar-title">
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
</select>
</li>
<li class="sidebar-title"><span>Endpoint actions</span></li>
<li class="sidebar-list">
<a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
</li>
<li class="sidebar-title"><span>Portainer settings</span></li>
<li class="sidebar-list">
<a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
</li>
</ul>
<div class="sidebar-footer">
<div class="col-xs-12">
<a href="https://github.com/portainer/portainer" target="_blank">
<i class="fa fa-github" aria-hidden="true"></i>
Portainer {{ uiVersion }}
</a>
</div>
</div>
</div>
<!-- End Sidebar -->
+38 -7
View File
@@ -1,11 +1,42 @@
angular.module('sidebar', [])
.controller('SideBarController', ['$scope', 'Container', 'Settings',
function($scope, Container, Settings) {
$scope.template = 'partials/sidebar.html';
$scope.containers = [];
$scope.endpoint = Settings.endpoint;
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
Container.query({all: 0}, function(d) {
$scope.containers = d;
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
$scope.uiVersion = Settings.uiVersion;
$scope.switchEndpoint = function(endpoint) {
EndpointService.setActive(endpoint.Id).then(function success(data) {
StateManager.updateEndpointState(true)
.then(function success() {
$state.reload();
}, function error(err) {
Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
});
}, function error(err) {
Messages.error("Failure", err, "Unable to switch to new endpoint");
});
};
function fetchEndpoints() {
EndpointService.endpoints().then(function success(data) {
$scope.endpoints = data;
EndpointService.getActive().then(function success(data) {
angular.forEach($scope.endpoints, function (endpoint) {
if (endpoint.Id === data.Id) {
$scope.activeEndpoint = endpoint;
}
});
}, function error(err) {
Messages.error("Failure", err, "Unable to retrieve active endpoint");
});
}, function error(err) {
$scope.endpoints = [];
});
}
fetchEndpoints();
}]);
@@ -1,53 +0,0 @@
angular.module('startContainer', [])
.controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages',
function($scope, $routeParams, $location, Container, Messages) {
$scope.template = 'app/components/startContainer/startcontainer.html';
$scope.config = {
name: '',
memory: 0,
memorySwap: 0,
cpuShares: 1024,
env: '',
commands: '',
volumesFrom: ''
};
$scope.commandPlaceholder = '["/bin/echo", "Hello world"]';
function failedRequestHandler(e, Messages) {
Messages.send({class: 'text-error', data: e.data});
}
$scope.create = function() {
var cmds = null;
if ($scope.config.commands !== '') {
cmds = angular.fromJson($scope.config.commands);
}
var id = $routeParams.id;
var ctor = Container;
var loc = $location;
var s = $scope;
Container.create({
Image: id,
name: $scope.config.name,
Memory: $scope.config.memory,
MemorySwap: $scope.config.memorySwap,
CpuShares: $scope.config.cpuShares,
Cmd: cmds,
VolumesFrom: $scope.config.volumesFrom
}, function(d) {
if (d.Id) {
ctor.start({id: d.Id}, function(cd) {
$('#create-modal').modal('hide');
loc.path('/containers/' + d.Id + '/');
}, function(e) {
failedRequestHandler(e, Messages);
});
} else {
failedRequestHandler(d, Messages);
}
}, function(e) {
failedRequestHandler(e, Messages);
});
};
}]);
@@ -1,44 +0,0 @@
<div id="create-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Create And Start Container From Image</h3>
</div>
<div class="modal-body">
<form role="form">
<fieldset>
<div class="form-group">
<label>Cmd:</label>
<input type="text" placeholder="{{ commandPlaceholder }}" ng-model="config.commands" class="form-control"/>
<small>Input commands as an array</small>
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="config.name" class="form-control"/>
</div>
<div class="form-group">
<label>Memory:</label>
<input type="number" ng-model="config.memory" class="form-control"/>
</div>
<div class="form-group">
<label>Memory Swap:</label>
<input type="number" ng-model="config.memorySwap" class="form-control"/>
</div>
<div class="form-group">
<label>CPU Shares:</label>
<input type="number" ng-model="config.cpuShares" class="form-control"/>
</div>
<div class="form-group">
<label>Volumes From:</label>
<input type="text" ng-model="config.volumesFrom" class="form-control"/>
</div>
</fieldset>
</form>
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary" ng-click="create()">Create</a>
</div>
</div>
</div>
</div>
+94
View File
@@ -0,0 +1,94 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
<rd-widget-body>
<canvas id="cpu-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
<rd-widget-body>
<canvas id="memory-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
<rd-widget-body>
<canvas id="network-stats-chart" width="770" height="230"></canvas>
<div class="comment">
<div id="network-legend" style="margin-bottom: 20px;"></div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in containerTop.Titles">
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
{{title}}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr>
</tbody>
</table>
<div ng-if="containerTop.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+215
View File
@@ -0,0 +1,215 @@
angular.module('stats', [])
.controller('StatsController', ['Pagination', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory
$scope.ps_args = '';
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.getTop = function () {
ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args
}, function (data) {
$scope.containerTop = data;
});
};
$document.ready(function(){
var cpuLabels = [];
var cpuData = [];
var memoryLabels = [];
var memoryData = [];
var networkLabels = [];
var networkTxData = [];
var networkRxData = [];
for (var i = 0; i < 20; i++) {
cpuLabels.push('');
cpuData.push(0);
memoryLabels.push('');
memoryData.push(0);
networkLabels.push('');
networkTxData.push(0);
networkRxData.push(0);
}
var cpuDataset = { // CPU Usage
fillColor: "rgba(151,187,205,0.5)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
data: cpuData
};
var memoryDataset = {
fillColor: "rgba(151,187,205,0.5)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
data: memoryData
};
var networkRxDataset = {
label: "Rx Bytes",
fillColor: "rgba(151,187,205,0.5)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
data: networkRxData
};
var networkTxDataset = {
label: "Tx Bytes",
fillColor: "rgba(255,180,174,0.5)",
strokeColor: "rgba(255,180,174,1)",
pointColor: "rgba(255,180,174,1)",
pointStrokeColor: "#fff",
data: networkTxData
};
var networkLegendData = [
{
//value: '',
color: 'rgba(151,187,205,0.5)',
title: 'Rx Data'
},
{
//value: '',
color: 'rgba(255,180,174,0.5)',
title: 'Tx Data'
}
];
legend($('#network-legend').get(0), networkLegendData);
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({
labels: cpuLabels,
datasets: [cpuDataset]
}, {
responsive: true
});
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
labels: memoryLabels,
datasets: [memoryDataset]
},
{
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
//scaleOverride: true,
//scaleSteps: 10,
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
//scaleStartValue: 0
});
var networkChart = new Chart($('#network-stats-chart').get(0).getContext("2d")).Line({
labels: networkLabels,
datasets: [networkRxDataset, networkTxDataset]
}, {
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
});
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
function setUpdateStatsTimeout() {
if(!destroyed) {
timeout = $timeout(updateStats, 5000);
}
}
function updateStats() {
Container.stats({id: $stateParams.id}, function (d) {
var arr = Object.keys(d).map(function (key) {
return d[key];
});
if (arr.join('').indexOf('no such id') !== -1) {
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
return;
}
// Update graph with latest data
$scope.data = d;
updateCpuChart(d);
updateMemoryChart(d);
updateNetworkChart(d);
setUpdateStatsTimeout();
}, function () {
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
setUpdateStatsTimeout();
});
}
var destroyed = false;
var timeout;
$scope.$on('$destroy', function () {
destroyed = true;
$timeout.cancel(timeout);
});
updateStats();
function updateCpuChart(data) {
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
cpuChart.removeData();
}
function updateMemoryChart(data) {
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
memoryChart.removeData();
}
var lastRxBytes = 0, lastTxBytes = 0;
function updateNetworkChart(data) {
// 1.9+ contains an object of networks, for now we'll just show stats for the first network
// TODO: Show graphs for all networks
if (data.networks) {
$scope.networkName = Object.keys(data.networks)[0];
data.network = data.networks[$scope.networkName];
}
if(data.network) {
var rxBytes = 0, txBytes = 0;
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
// These will be zero on first call, ignore to prevent large graph spike
rxBytes = data.network.rx_bytes - lastRxBytes;
txBytes = data.network.tx_bytes - lastTxBytes;
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
}
function calculateCPUPercent(stats) {
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
var prevCpu = stats.precpu_stats;
var curCpu = stats.cpu_stats;
var cpuPercent = 0.0;
// calculate the change for the cpu usage of the container in between readings
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
// calculate the change for the entire system between readings
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
}
return cpuPercent;
}
});
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve container info");
});
$scope.getTop();
}]);
+226
View File
@@ -0,0 +1,226 @@
<rd-header>
<rd-header-title title="Cluster overview">
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Swarm</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td>
</tr>
<tr>
<td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Strategy</td>
<td>{{ swarm.Strategy }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th>
<a ui-sref="swarm" ng-click="order('name')">
Name
<span ng-show="sortType == 'name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('cpu')">
CPU
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('memory')">
Memory
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('ip')">
IP
<span ng-show="sortType == 'ip' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ip' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('version')">
Engine
<span ng-show="sortType == 'version' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'version' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('status')">
Status
<span ng-show="sortType == 'status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td>{{ node.name }}</td>
<td>{{ node.cpu }}</td>
<td>{{ node.memory }}</td>
<td>{{ node.ip }}</td>
<td>{{ node.version }}</td>
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
</tr>
</tbody>
</table>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th>
<a ui-sref="swarm" ng-click="order('Description.Hostname')">
Name
<span ng-show="sortType == 'Description.Hostname' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Hostname' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Spec.Role')">
Role
<span ng-show="sortType == 'Spec.Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Spec.Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Description.Resources.NanoCPUs')">
CPU
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Description.Resources.MemoryBytes')">
Memory
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Description.Engine.EngineVersion')">
Engine
<span ng-show="sortType == 'Description.Engine.EngineVersion' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Engine.EngineVersion' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('node.Status.State')">
Status
<span ng-show="sortType == 'node.Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'node.Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="node({id: node.ID})">{{ node.Description.Hostname }}</a></td>
<td>{{ node.Spec.Role }}</td>
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
<td>{{ node.Description.Engine.EngineVersion }}</td>
<td><span class="label label-{{ node.Status.State|nodestatusbadge }}">{{ node.Status.State }}</span></td>
</tr>
</tbody>
</table>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+85
View File
@@ -0,0 +1,85 @@
angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination',
function ($scope, Info, Version, Node, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
$scope.sortType = 'Spec.Role';
$scope.sortReverse = false;
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.totalCPU = 0;
$scope.totalMemory = 0;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count);
};
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.query({}, function(d) {
$scope.nodes = d;
var CPU = 0, memory = 0;
angular.forEach(d, function(node) {
CPU += node.Description.Resources.NanoCPUs;
memory += node.Description.Resources.MemoryBytes;
});
$scope.totalCPU = CPU / 1000000000;
$scope.totalMemory = memory;
});
} else {
extractSwarmInfo(d);
}
});
function extractSwarmInfo(info) {
// Swarm info is available in SystemStatus object
var systemStatus = info.SystemStatus;
// Swarm strategy
$scope.swarm[systemStatus[1][0]] = systemStatus[1][1];
// Swarm filters
$scope.swarm[systemStatus[2][0]] = systemStatus[2][1];
// Swarm node count
var nodes = systemStatus[0][1] === 'primary' ? systemStatus[3][1] : systemStatus[4][1];
var node_count = parseInt(nodes, 10);
$scope.swarm[systemStatus[3][0]] = node_count;
$scope.swarm.Status = [];
extractNodesInfo(systemStatus, node_count);
}
function extractNodesInfo(info, node_count) {
// First information for node1 available at element #4 of SystemStatus if connected to a primary
// If connected to a replica, information for node1 is available at element #5
// The next 10 elements are information related to the node
var node_offset = info[0][1] === 'primary' ? 4 : 5;
for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset);
node_offset += 9;
}
}
function extractNodeInfo(info, offset) {
var node = {};
node.name = info[offset][0];
node.ip = info[offset][1];
node.id = info[offset + 1][1];
node.status = info[offset + 2][1];
node.containers = info[offset + 3][1];
node.cpu = info[offset + 4][1].split('/')[1];
node.memory = info[offset + 5][1].split('/')[1];
node.labels = info[offset + 6][1];
node.version = info[offset + 8][1];
$scope.swarm.Status.push(node);
}
}]);
+50
View File
@@ -0,0 +1,50 @@
<rd-header>
<rd-header-title title="Task details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }}
</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-tasks" title="Task status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>{{ task.ID }}</td>
</tr>
<tr>
<td>State</td>
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
</tr>
<tr ng-if="task.Status.Err">
<td>Error message</td>
<td><code>{{ task.Status.Err }}</code></td>
</tr>
<tr>
<td>Image</td>
<td>{{ task.Spec.ContainerSpec.Image }}</td>
</tr>
<tr>
<td>Slot</td>
<td>{{ task.Slot }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ task.CreatedAt|getisodate }}</td>
</tr>
<tr ng-if="task.Status.ContainerStatus.ContainerID">
<td>Container ID</td>
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More