Compare commits

...

410 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
145 changed files with 11553 additions and 3109 deletions

44
.github/ISSUE_TEMPLATE.md vendored Normal file
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:

10
.gitignore vendored
View File

@@ -1,10 +1,6 @@
logs/*
!.gitkeep
*.esproj/*
node_modules
bower_components
.idea
*.iml
dist
dist/*
ui-for-docker-checksum.txt
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp

2
.godir
View File

@@ -1 +1 @@
dockerui
portainer

77
CONTRIBUTING.md Normal file
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

View File

@@ -1,6 +0,0 @@
FROM scratch
COPY dist /
EXPOSE 9000
ENTRYPOINT ["/ui-for-docker"]

34
LICENSE
View File

@@ -1,24 +1,24 @@
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
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.
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
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

View File

@@ -1 +0,0 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"

View File

@@ -1,35 +1,52 @@
## Cloudinovasi UI for Docker
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
</p>
![Dashboard](/dashboard.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)
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
### Goals
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
**_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).
### Quickstart
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
**_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*.
2. Open your browser to `http://<dockerd host ip>:9000`
## Demo
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
The `--privileged` flag is required for hosts using SELinux.
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**).
### Specify socket to connect to Docker daemon
Please note that the public demo cluster is **reset every 15min**.
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
## Getting started
You can use the `-e` flag to change this socket:
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 --privileged cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
## Getting help
### Change address/port UI For Docker is served on
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
* 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/
# Expose UI For Docker on 10.20.30.1:80
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
## 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!
## Limitations
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
* Docker Swarm >= 1.2.3
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9

72
api/bolt/datastore.go Normal file
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
}

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
})
}

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
api/bolt/user_service.go Normal file
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
api/cli/cli.go Normal file
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
api/cli/defaults.go Normal file
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"
)

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
api/cli/pairlist.go Normal file
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
api/cmd/portainer/main.go Normal file
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
api/crypto/crypto.go Normal file
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
api/errors.go Normal file
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
api/file/file.go Normal file
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
api/http/auth_handler.go Normal file
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
api/http/docker_handler.go Normal file
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)
}
}

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
api/http/file_handler.go Normal file
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
api/http/handler.go Normal file
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
api/http/middleware.go Normal file
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
api/http/server.go Normal file
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)
}

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)
}

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
api/http/tls.go Normal file
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
}

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
api/http/user_handler.go Normal file
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"`
}

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
api/jwt/jwt.go Normal file
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
api/portainer.go Normal file
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
)

View File

@@ -1,90 +1,531 @@
angular.module('uifordocker', [
'uifordocker.templates',
angular.module('portainer', [
'portainer.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'dockerui.services',
'dockerui.filters',
'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',
'pullImage',
'startContainer',
'containerLogs',
'main',
'service',
'services',
'settings',
'sidebar',
'createService',
'stats',
'swarm',
'network',
'networks',
'node',
'createNetwork',
'task',
'templates',
'volumes',
'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
'use strict';
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
localStorageServiceProvider
.setStorageType('sessionStorage')
.setPrefix('portainer');
$urlRouterProvider.otherwise('/');
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('index', {
url: '/',
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
.state('auth', {
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
"content": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
}
})
.state('containers', {
url: '/containers/',
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
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",
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
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",
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
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",
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
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/',
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
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/',
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
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/',
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
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/',
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
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/',
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
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/',
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
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
@@ -92,25 +533,39 @@ angular.module('uifordocker', [
return {
'response': function(response) {
if (typeof(response.data) === 'string' &&
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
$.gritter.add({
title: 'Error',
text: $('<div>').text(response.data).html(),
time: 10000
});
}
var csrfToken = response.headers('X-Csrf-Token');
if (csrfToken) {
document.cookie = 'csrfToken=' + csrfToken;
}
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_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', 'v1.0.2');
.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');

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>

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';
});
};
}]);

View File

@@ -1,72 +1,25 @@
<rd-header>
<rd-header-title title="Container details"></rd-header-title>
<rd-header-title title="Container details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
</div>
<div ng-if="!container.edit">
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name <a href="" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</div>
</div>
<div ng-if="container.edit">
<div class="title"><input type="text" class="containerNameInput" ng-model="container.newContainerName"></div>
<div class="comment">
Name
<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>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div ng-class="{true: 'widget-icon green pull-left', false: 'widget-icon red pull-left'}[container.State.Running]">
<i class="fa fa-heartbeat"></i>
</div>
<div class="title">{{ container.State|getstatetext }}</div>
<div class="comment">State</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-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="commit()">Commit</button>
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
<button class="btn btn-primary" ng-click="restart()">Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
</div>
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
</div>
</div>
<div class="comment">
Actions
<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>
</rd-widget-body>
</rd-widget>
@@ -74,44 +27,146 @@
</div>
<div class="row">
<div class="col-lg-9">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Created</td>
<td>{{ container.Created | date: 'medium' }}</td>
<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>Path</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args</td>
<td>{{ container.Args.join(' ') || 'None' }}</td>
</tr>
<tr>
<td>Exposed Ports</td>
<td>Status</td>
<td>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
<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>Environment</td>
<td>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
<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 role="table" class="table">
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
@@ -119,76 +174,94 @@
</table>
</td>
</tr>
<tr>
<td>Publish all ports</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
<tr>
<td>Ports</td>
<tr ng-if="container.HostConfig.RestartPolicy.Name !== 'no'">
<td>Restart policies</td>
<td>
<ul>
<li ng-repeat="(containerport, hostports) in container.NetworkSettings.Ports">
{{ containerport }} =>
<span class="label label-default" style="margin-right: 5px;" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
</li>
</ul>
<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>
<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.join(' ') }}</td>
</tr>
<tr>
<td>Bindings</td>
<td>
<ul>
<li ng-repeat="b in container.HostConfig.Binds">{{ b }}</li>
</ul>
</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 ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3">
</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-tasks" title="Container state details"></rd-widget-header>
<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="(key, val) in container.State">
<td>{{key}}</td>
<td>{{ val }}</td>
<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>
<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>

View File

@@ -1,208 +1,180 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', 'ViewSpinner', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, ViewSpinner, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
$scope.editBinds = false;
$scope.newCfg = {
Env: [],
Ports: {}
.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');
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('container_networks', $scope.state.pagination_count);
};
var update = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
// fill up env
if (d.Config.Env) {
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
return {name: entry.split('=')[0], value: entry.split('=')[1]};
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);
}
});
}
// fill up ports
$scope.newCfg.Ports = {};
angular.forEach(d.Config.ExposedPorts, function(i, port) {
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
}
else {
$scope.newCfg.Ports[port] = [];
}
});
// fill up bindings
$scope.newCfg.Binds = [];
var defaultBinds = {};
angular.forEach(d.Config.Volumes, function(value, vol) {
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
});
angular.forEach(d.HostConfig.Binds, function(binding, i) {
var mountpoint = binding.split(':')[0];
var vol = binding.split(':')[1] || '';
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
var defaultBind = false;
if (vol === '') {
vol = mountpoint;
mountpoint = '';
}
if (vol in defaultBinds) {
delete defaultBinds[vol];
defaultBind = true;
}
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
});
angular.forEach(defaultBinds, function(bind) {
$scope.newCfg.Binds.push(bind);
});
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#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) {
$('#loadingViewSpinner').show();
Container.start({id: $scope.container.Id}, {}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to start." + e.data);
Messages.error("Failure", e, "Unable to start container");
});
};
$scope.stop = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.stop({id: $stateParams.id}, function (d) {
update();
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to stop." + e.data);
Messages.error("Failure", e, "Unable to stop container");
});
};
$scope.kill = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.kill({id: $stateParams.id}, function (d) {
update();
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to die." + e.data);
Messages.error("Failure", e, "Unable to kill container");
});
};
$scope.commit = function () {
ViewSpinner.spin();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
$('#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", "Container failed to commit." + e.data);
Messages.error("Failure", e, "Unable to commit container");
});
};
$scope.pause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to pause." + e.data);
Messages.error("Failure", e, "Unable to pause container");
});
};
$scope.unpause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.unpause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to unpause." + e.data);
Messages.error("Failure", e, "Unable to unpause container");
});
};
$scope.remove = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
update();
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
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", "Container failed to remove." + e.data);
Messages.error("Failure", e, "Unable to remove container");
});
};
$scope.restart = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.restart({id: $stateParams.id}, function (d) {
update();
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to restart." + e.data);
});
};
$scope.hasContent = function (data) {
return data !== null && data !== undefined;
};
$scope.getChanges = function () {
ViewSpinner.spin();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
ViewSpinner.stop();
Messages.error("Failure", e, "Unable to restart container");
});
};
$scope.renameContainer = function () {
// #FIXME fix me later to handle http status to show the correct error message
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
if (data.name) {
$scope.container.Name = data.name;
Messages.send("Container renamed", $stateParams.id);
} else {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Failure", "Container failed to rename.");
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.addEntry = function (array, entry) {
array.push(entry);
};
$scope.rmEntry = function (array, entry) {
var idx = array.indexOf(entry);
array.splice(idx, 1);
};
$scope.toggleEdit = function() {
$scope.edit = !$scope.edit;
$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();
$scope.getChanges();
}]);

View File

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

View File

@@ -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;
};
};
}
}]);

View File

@@ -1,6 +1,6 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
@@ -8,24 +8,20 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpi
$scope.stderr = '';
$scope.tailLines = 2000;
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
function getLogs() {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
getLogsStdout();
getLogsStderr();
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}
function getLogsStderr() {

View File

@@ -1,7 +1,9 @@
<rd-header>
<rd-header-title title="Container logs"></rd-header-title>
<rd-header-title title="Container logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>
</rd-header>
@@ -10,7 +12,7 @@
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div>

View File

@@ -1,42 +1,60 @@
<div ng-include="template" ng-controller="StartContainerController"></div>
<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>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers">
<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" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button>
<button type="button" class="btn btn-primary" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</button>
<button type="button" class="btn btn-primary" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</button>
<button type="button" class="btn btn-primary" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</button>
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#create-modal">Start a new container...</button>
<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()"/><label for="displayAll">Display All</label>
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></th>
<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
@@ -51,40 +69,56 @@
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Command')">
Command
<span ng-show="sortType == 'Command' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<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('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>
<th>
<a ui-sref="containers" 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 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 ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
<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><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></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>{{ container.Command|truncate:40 }}</td>
<td>{{ container.Created|getdate }}</td>
<td><span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span></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>

View File

@@ -1,86 +1,125 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner',
function ($scope, Container, Settings, Messages, ViewSpinner) {
.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.sortType = 'Created';
$scope.sortReverse = true;
$scope.state.toggle = false;
$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;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
var update = function (data) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
$scope.containers = d.filter(function (container) {
return container.Image !== 'swarm';
}).map(function (container) {
return new ContainerViewModel(container);
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;
});
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
}, function (e) {
$('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers");
$scope.containers = [];
});
};
var batch = function (items, action, msg) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
update({all: Settings.displayAll ? 1 : 0});
}
};
angular.forEach(items, function (c) {
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
Container.get({id: c.Id}, function (d) {
c = d;
counter = counter + 1;
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
Messages.send("Container " + msg, c.Id);
var index = $scope.containers.indexOf(c);
complete();
}, function (e) {
Messages.error("Failure", e.data);
complete();
});
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
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 {
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);
Messages.error("Failure", e, 'An error occured');
complete();
});
}
}
});
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
}
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredContainers, function (container) {
if (container.Checked !== allSelected) {
container.Checked = allSelected;
$scope.selectItem(container);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -89,18 +128,6 @@ function ($scope, Container, Settings, Messages, ViewSpinner) {
}
};
$scope.toggleSelectAll = function () {
$scope.state.selectedItem = $scope.state.toggle;
angular.forEach($scope.state.filteredContainers, function (i) {
i.Checked = $scope.state.toggle;
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredContainers.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.toggleGetAll = function () {
Settings.displayAll = $scope.state.displayAll;
update({all: Settings.displayAll ? 1 : 0});
@@ -134,5 +161,30 @@ function ($scope, Container, Settings, Messages, ViewSpinner) {
batch($scope.containers, Container.remove, "Removed");
};
update({all: Settings.displayAll ? 1 : 0});
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});
}
});
}]);

View File

@@ -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);
}
};
}]);

View File

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

View File

@@ -1,45 +0,0 @@
<div id="create-network-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"><i class="fa fa-times" aria-hidden="true"></i></button>
<h3>Create network</h3>
</div>
<div class="modal-body">
<form novalidate role="form" name="createNetworkForm">
<div class="form-group">
<label>Name:</label>
<input type="text" placeholder='my_network'
ng-model="createNetworkConfig.Name" class="form-control"/>
</div>
<div class="form-group">
<label>Driver:</label>
<input type="text" placeholder='bridge'
ng-model="createNetworkConfig.Driver" class="form-control"/>
</div>
<div class="form-group">
<label>Subnet:</label>
<input type="text" placeholder='172.20.0.0/16'
ng-model="createNetworkConfig.IPAM.Config[0].Subnet" class="form-control"/>
</div>
<div class="form-group">
<label>IPRange:</label>
<input type="text" placeholder='172.20.10.0/24'
ng-model="createNetworkConfig.IPAM.Config[0].IPRange" class="form-control"/>
</div>
<div class="form-group">
<label>Gateway:</label>
<input type="text" placeholder='172.20.10.11'
ng-model="createNetworkConfig.IPAM.Config[0].Gateway" class="form-control"/>
</div>
</form>
</div>
<div class="alert alert-error" id="error-message" style="display:none">
{{ error }}
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary" ng-click="createNetwork(createNetworkConfig)">Create</a>
</div>
</div>
</div>
</div>

View File

@@ -1,41 +1,98 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
$scope.template = 'app/components/createNetwork/createNetwork.html';
$scope.init = function () {
$scope.createNetworkConfig = {
"Name": '',
"Driver": '',
"IPAM": {
"Config": [{}]
}
};
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: '',
Labels: []
};
$scope.init();
$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.createNetwork = function addNetwork(createNetworkConfig) {
if (_.isEmpty(createNetworkConfig.IPAM.Config[0])) {
delete createNetworkConfig.IPAM;
}
$('#error-message').hide();
ViewSpinner.spin();
$('#create-network-modal').modal('hide');
Network.create(createNetworkConfig, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
$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.error('Failure', errorMsgFilter(d));
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
ViewSpinner.stop();
$scope.init();
$state.go('networks', {}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$scope.error = "Cannot pull image " + imageName + " Reason: " + e.data;
$('#create-network-modal').modal('show');
$('#error-message').show();
$('#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);
};
}]);

View File

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

View File

@@ -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");
});
}]);

View File

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

View File

@@ -1,40 +0,0 @@
<div id="create-volume-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"><i class="fa fa-times" aria-hidden="true"></i></button>
<h3>Create volume</h3>
</div>
<div class="modal-body">
<form novalidate role="form" name="createVolumeForm">
<div class="form-group">
<label>Name:</label>
<input type="text" placeholder='my_volume'
ng-model="createVolumeConfig.Name" class="form-control"/>
</div>
<div class="form-group">
<label>Driver:</label>
<ui-select ng-model="selectedDriver.value" theme="bootstrap">
<ui-select-match>
<span ng-bind="$select.selected"></span>
</ui-select-match>
<ui-select-choices repeat="driver in availableDrivers">
<span ng-bind="driver"></span>
</ui-select-choices>
</ui-select>
</div>
<div class="form-group" ng-show="selectedDriver.value == 'local-persist'">
<label>Mount point:</label>
<input type="text" ng-model="createVolumeConfig.DriverOpts.mountpoint" name="" placeholder="/volume/my_volume" class="form-control">
</div>
</form>
</div>
<div class="alert alert-error" id="error-message" style="display:none">
{{ error }}
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary" ng-click="addVolume(createVolumeConfig)">Create</a>
</div>
</div>
</div>
</div>

View File

@@ -1,39 +1,56 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Messages', 'Volume', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Messages, Volume, ViewSpinner, errorMsgFilter) {
$scope.template = 'app/components/createVolume/createVolume.html';
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$scope.init = function () {
$scope.createVolumeConfig = {
"Name": "",
"Driver": "",
"DriverOpts": {}
};
$scope.availableDrivers = ['local', 'local-persist'];
$scope.selectedDriver = { value: $scope.availableDrivers[0] };
$scope.formValues = {
DriverOptions: []
};
$scope.init();
$scope.config = {
Driver: 'local'
};
$scope.addVolume = function addVolume(createVolumeConfig) {
$('#error-message').hide();
ViewSpinner.spin();
$('#create-volume-modal').modal('hide');
createVolumeConfig.Driver = $scope.selectedDriver.value;
console.log(JSON.stringify(createVolumeConfig, null, 4));
Volume.create(createVolumeConfig, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
$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.error('Failure', errorMsgFilter(d));
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
ViewSpinner.stop();
$state.go('volumes', {}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$scope.error = "Cannot create volume " + createVolumeConfig.Name + " Reason: " + e.data;
$('#create-volume-modal').modal('show');
$('#error-message').show();
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.DriverOpts = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createVolume(config);
};
}]);

View File

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

View File

@@ -1,76 +1,153 @@
<rd-header>
<rd-header-title title="Home"></rd-header-title>
<rd-header-title title="Home">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
<rd-widget-header icon="fa-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-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-body>
<div class="widget-icon green pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.running }}</div>
<div class="comment">Running</div>
<rd-widget-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-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-body>
<div class="widget-icon red pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.stopped }}</div>
<div class="comment">Stopped</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon gray pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.ghost }}</div>
<div class="comment">Ghost</div>
<rd-widget-header icon="fa-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">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers created"></rd-widget-header>
<rd-widget-body>
<canvas id="containers-started-chart" width="770" height="230">
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
</canvas>
</rd-widget-body>
</rd-widget>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="containers">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-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">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images created"></rd-widget-header>
<rd-widget-body>
<canvas id="images-created-chart" width="770" height="230">
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
</canvas>
</rd-widget-body>
</rd-widget>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="images">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-clone"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-pie-chart 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>

View File

@@ -1,46 +1,92 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Messages',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Messages) {
$scope.containerData = {};
var buildCharts = function (data) {
$scope.containerData.total = data.length;
LineChart.build('#containers-started-chart', data, function (c) {
return new Date(c.Created * 1000).toLocaleDateString();
});
var s = $scope;
Image.query({}, function (d) {
s.totalImages = d.length;
LineChart.build('#images-created-chart', d, function (c) {
return new Date(c.Created * 1000).toLocaleDateString();
});
});
$scope.containerData = {
total: 0
};
$scope.imageData = {
total: 0
};
$scope.networkData = {
total: 0
};
$scope.volumeData = {
total: 0
};
Container.query({all: 1}, function (d) {
function prepareContainerData(d, containersToHideLabels) {
var running = 0;
var ghost = 0;
var stopped = 0;
// TODO: centralize that
var containers = d.filter(function (container) {
return container.Image !== 'swarm';
});
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
if (item.Status === "Ghost") {
ghost += 1;
if (item.Status.indexOf('Up') !== -1) {
running += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.ghost = ghost;
$scope.containerData.total = containers.length;
}
buildCharts(containers);
function prepareImageData(d) {
var images = d;
var totalImageSize = 0;
for (var i = 0; i < images.length; i++) {
var item = images[i];
totalImageSize += item.VirtualSize;
}
$scope.imageData.total = images.length;
$scope.imageData.size = totalImageSize;
}
function prepareVolumeData(d) {
var volumes = d.Volumes;
if (volumes) {
$scope.volumeData.total = volumes.length;
}
}
function prepareNetworkData(d) {
var networks = d;
$scope.networkData.total = networks.length;
}
function prepareInfoData(d) {
var info = d;
$scope.infoData = info;
}
function fetchDashboardData(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);
});
}]);

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>

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();
});
}]);

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>

View File

@@ -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);
}]);

View File

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

View File

@@ -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;
}
});
};
}]);

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>

View File

@@ -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();
}]);

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>

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");
});
}]);

View File

@@ -1,97 +1,159 @@
<rd-header>
<rd-header-title title="Image details"></rd-header-title>
<rd-header-title title="Image details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Images > <a ui-sref="image({id: id})">{{ id }}</a>
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
<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="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>
<div class="widget-icon grey pull-left">
<i class="fa fa-clone"></i>
</div>
<div class="title">{{ id }}</div>
<div class="comment">Image ID</div>
<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>
</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>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-danger" ng-click="removeImage(id)">Remove</button>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-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>Created</td>
<td>{{ image.Created | date: 'medium'}}</td>
</tr>
<tr>
<td>Tags</td>
<td>ID</td>
<td>
<ul>
<li ng-repeat="tag in RepoTags">{{ tag }}
<button ng-click="removeImage(tag)" class="btn btn-sm btn-danger">Remove tag</button>
</li>
</ul>
{{ 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>
<tr ng-if="image.Parent">
<td>Parent</td>
<td><a ui-sref="image({id: image.Parent})">{{ image.Parent }}</a></td>
</tr>
<tr>
<td>Size (Virtual Size)</td>
<td>{{ image.Size|humansize }} ({{ image.VirtualSize|humansize }})</td>
</tr>
<tr>
<td>Hostname</td>
<td>{{ image.ContainerConfig.Hostname }}</td>
<td>Size</td>
<td>{{ image.VirtualSize|humansize }}</td>
</tr>
<tr>
<td>User</td>
<td>{{ image.ContainerConfig.User }}</td>
<td>Created</td>
<td>{{ image.Created|getisodate }}</td>
</tr>
<tr>
<td>Cmd</td>
<td>{{ image.ContainerConfig.Cmd }}</td>
</tr>
<tr>
<td>Volumes</td>
<td>{{ image.ContainerConfig.Volumes }}</td>
</tr>
<tr>
<td>Volumes from</td>
<td>{{ image.ContainerConfig.VolumesFrom }}</td>
</tr>
<tr>
<td>Built with</td>
<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>

View File

@@ -1,33 +1,14 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$q', '$stateParams', '$state', 'Image', 'Container', 'Messages', 'LineChart',
function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChart) {
$scope.tagInfo = {repo: '', version: '', force: false};
$scope.id = '';
$scope.repoTags = [];
$scope.removeImage = function (id) {
Image.remove({id: id}, function (d) {
d.forEach(function(msg){
var key = Object.keys(msg)[0];
Messages.send(key, msg[key]);
});
// If last message key is 'Deleted' then assume the image is gone and send to images page
if (d[d.length-1].Deleted) {
$state.go('images', {}, {reload: true});
} else {
$state.go('image', {id: $scope.id}, {reload: true});
}
}, function (e) {
$scope.error = e.data;
$('#error-message').show();
});
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
};
/**
* Get RepoTags from the /images/query endpoint instead of /image/json,
* for backwards compatibility with Docker API versions older than 1.21
* @param {string} imageId
*/
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
Image.query({}, function (d) {
d.forEach(function(image) {
@@ -38,21 +19,71 @@ function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChar
});
}
$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;
$scope.id = d.Id;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags($scope.id);
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
$scope.error = "Image not found.<br />" + $stateParams.id;
} else {
$scope.error = e.data;
}
$('#error-message').show();
Messages.error("Failure", e, "Unable to retrieve image info");
});
}]);

View File

@@ -1,77 +1,135 @@
<div ng-include="template" ng-controller="PullImageController"></div>
<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 class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images">
</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-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#pull-modal">Pull new image...</button>
</div>
</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">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></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')">
Repository
<span ng-show="sortType == 'RepoTags' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'RepoTags' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('VirtualSize')">
VirtualSize
<span ng-show="sortType == 'VirtualSize' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>{{ image|repotag }}</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getdate }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
<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>
<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>

View File

@@ -1,26 +1,33 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', 'Image', 'ViewSpinner', 'Messages',
function ($scope, Image, ViewSpinner, Messages) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
$scope.state = {};
$scope.sortType = 'Created';
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.config = {
Image: '',
Registry: ''
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.toggleSelectAll = function () {
angular.forEach($scope.state.filteredImages, function (i) {
i.Checked = $scope.state.toggle;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('images', $scope.state.pagination_count);
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredImages, function (image) {
if (image.Checked !== allSelected) {
image.Checked = allSelected;
$scope.selectItem(image);
}
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredImages.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.selectItem = function (item) {
@@ -31,27 +38,51 @@ function ($scope, Image, ViewSpinner, Messages) {
}
};
$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 () {
ViewSpinner.spin();
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}
};
angular.forEach($scope.images, function (i) {
if (i.Checked) {
counter = counter + 1;
Image.remove({id: i.Id}, function (d) {
angular.forEach(d, function (resource) {
Messages.send("Image deleted", resource.Deleted);
});
var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1);
if (d[0].message) {
$('#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.data);
Messages.error("Failure", e, 'Unable to remove image');
complete();
});
}
@@ -59,17 +90,19 @@ function ($scope, Image, ViewSpinner, Messages) {
};
function fetchImages() {
ViewSpinner.spin();
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve images");
$scope.images = [];
});
}
fetchImages();
Config.$promise.then(function (c) {
fetchImages();
});
}]);

View File

@@ -1,14 +1,17 @@
angular.module('dashboard')
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', function ($scope, $cookieStore, Settings) {
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'))) {
@@ -30,6 +33,4 @@ angular.module('dashboard')
window.onresize = function() {
$scope.$apply();
};
$scope.uiVersion = Settings.uiVersion;
}]);

View File

@@ -1,122 +1,93 @@
<rd-header>
<rd-header-title title="Network details"></rd-header-title>
<rd-header-title title="Network details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
<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-9 col-md-9 col-xs-9">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-sitemap"></i>
</div>
<div class="title">{{ network.Name }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-3 col-xs-3">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-default" disabled>Connect container...</button>
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<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>Id</td>
<td>{{ network.Id }}</td>
<td>Name</td>
<td>{{ network.Name }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ network.Scope }}</td>
<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>IPAM</td>
<td>
<table class="table table-striped">
<tr>
<td>Driver</td>
<td>{{ network.IPAM.Driver }}</td>
</tr>
<tr>
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr>
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr>
</table>
</td>
<td>Scope</td>
<td>{{ network.Scope }}</td>
</tr>
<tr>
<td>Containers</td>
<td>
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
<tr>
<td>Id</td>
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
</tr>
<tr>
<td>EndpointID</td>
<td>{{ container.EndpointID}}</td>
</tr>
<tr>
<td>MacAddress</td>
<td>{{ container.MacAddress}}</td>
</tr>
<tr>
<td>IPv4Address</td>
<td>{{ container.IPv4Address}}</td>
</tr>
<tr>
<td>IPv6Address</td>
<td>{{ container.IPv6Address}}</td>
</tr>
<tr>
<td colspan="2">
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
Disconnect from network
</button>
</td>
</tr>
</table>
</td>
<tr ng-if="network.IPAM.Config[0].Subnet">
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr>
<td>Options</td>
<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>
<table role="table" class="table table-striped">
<tr ng-repeat="(k, v) in network.Options">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
<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>

View File

@@ -1,37 +1,100 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, $state, $stateParams, errorMsgFilter) {
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) {
$scope.disconnect = function disconnect(networkId, containerId) {
ViewSpinner.spin();
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
ViewSpinner.stop();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
ViewSpinner.stop();
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
ViewSpinner.spin();
$scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
ViewSpinner.stop();
Messages.send("Network removed", "");
$state.go('networks', {});
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
}, function (e) {
ViewSpinner.stop();
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove network");
});
};
ViewSpinner.spin();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
ViewSpinner.stop();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$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();
});
}]);

View File

@@ -1,101 +1,159 @@
<div ng-include="template" ng-controller="CreateNetworkController"></div>
<rd-header>
<rd-header-title title="Network list">
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
<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="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
</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-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#create-network-modal">Create new network...</button>
</div>
</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">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></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 ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:20}}</a></td>
<td>{{ network.Id }}</td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
<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>

View File

@@ -1,27 +1,63 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', 'Network', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
function ($scope, $state, Network, Config, Messages, Pagination) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$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.toggleSelectAll = function () {
angular.forEach($scope.state.filteredNetworks, function (i) {
i.Checked = $scope.state.toggle;
$scope.selectItems = function(allSelected) {
angular.forEach($scope.state.filteredNetworks, function (network) {
if (network.Checked !== allSelected) {
network.Checked = allSelected;
$scope.selectItem(network);
}
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredNetworks.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.selectItem = function (item) {
@@ -33,24 +69,28 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}
};
angular.forEach($scope.networks, function (network) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
Messages.send("Network deleted", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
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.data);
Messages.error("Failure", e, 'Unable to remove network');
complete();
});
}
@@ -58,14 +98,18 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
};
function fetchNetworks() {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d;
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
$scope.networks = [];
});
}
fetchNetworks();
Config.$promise.then(function (c) {
fetchNetworks();
});
}]);

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>

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();
}]);

View File

@@ -1,35 +0,0 @@
<div id="pull-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"><i class="fa fa-times" aria-hidden="true"></i></button>
<h3>Pull image</h3>
</div>
<div class="modal-body">
<form novalidate role="form" name="pullForm">
<div class="form-group">
<label>Registry:</label>
<input type="text" ng-model="config.registry" class="form-control"
placeholder="Leave empty to user DockerHub"/>
</div>
<div class="form-group">
<label>Image Name:</label>
<input type="text" ng-model="config.fromImage" class="form-control" placeholder="username/image"
required/>
</div>
<div class="form-group">
<label>Tag Name:</label>
<input type="text" ng-model="config.tag" class="form-control"
placeholder="Leave empty to download ALL tags."/>
</div>
</form>
</div>
<div class="alert alert-error" id="error-message" style="display:none">
{{ error }}
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary" ng-click="pull()">Pull</a>
</div>
</div>
</div>
</div>

View File

@@ -1,56 +0,0 @@
angular.module('pullImage', [])
.controller('PullImageController', ['$scope', '$state', 'Messages', 'Image', 'ViewSpinner',
function ($scope, $state, Messages, Image, ViewSpinner) {
$scope.template = 'app/components/pullImage/pullImage.html';
$scope.init = function () {
$scope.config = {
registry: '',
fromImage: '',
tag: 'latest'
};
};
$scope.init();
function failedRequestHandler(e, Messages) {
Messages.error('Error', errorMsgFilter(e));
}
$scope.pull = function () {
$('#error-message').hide();
var config = angular.copy($scope.config);
var imageName = (config.registry ? config.registry + '/' : '' ) +
(config.fromImage) +
(config.tag ? ':' + config.tag : '');
ViewSpinner.spin();
$('#pull-modal').modal('hide');
Image.create(config, function (data) {
ViewSpinner.stop();
if (data.constructor === Array) {
var f = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
//check for error
if (f) {
var d = data[data.length - 1];
$scope.error = "Cannot pull image " + imageName + " Reason: " + d.error;
$('#pull-modal').modal('show');
$('#error-message').show();
} else {
Messages.send("Image Added", imageName);
$scope.init();
$state.go('images', {}, {reload: true});
}
} else {
Messages.send("Image Added", imageName);
$scope.init();
$state.go('images', {}, {reload: true});
}
}, function (e) {
ViewSpinner.stop();
$scope.error = "Cannot pull image " + imageName + " Reason: " + e.data;
$('#pull-modal').modal('show');
$('#error-message').show();
});
};
}]);

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>

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();
}]);

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>

View File

@@ -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();
}]);

View File

@@ -0,0 +1,67 @@
<rd-header>
<rd-header-title title="Settings">
</rd-header-title>
<rd-header-content>Settings</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-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>

View File

@@ -0,0 +1,30 @@
angular.module('settings', [])
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages',
function ($scope, $state, $sanitize, Users, Messages) {
$scope.formValues = {
currentPassword: '',
newPassword: '',
confirmPassword: ''
};
$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");
});
};
}]);

View File

@@ -0,0 +1,66 @@
<!-- 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 -->

View File

@@ -0,0 +1,42 @@
angular.module('sidebar', [])
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
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();
}]);

View File

@@ -1,163 +0,0 @@
angular.module('startContainer', ['ui.bootstrap'])
.controller('StartContainerController', ['$scope', '$state', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', 'ViewSpinner',
function ($scope, $state, Container, Messages, containernameFilter, errorMsgFilter, ViewSpinner) {
$scope.template = 'app/components/startContainer/startcontainer.html';
Container.query({all: 1}, function (d) {
$scope.containerNames = d.map(function (container) {
return containernameFilter(container);
});
});
$scope.config = {
Env: [],
Labels: [],
Volumes: [],
SecurityOpts: [],
HostConfig: {
PortBindings: [],
Binds: [],
Links: [],
Dns: [],
DnsSearch: [],
VolumesFrom: [],
CapAdd: [],
CapDrop: [],
Devices: [],
LxcConf: [],
ExtraHosts: []
}
};
$scope.menuStatus = {
containerOpen: true,
hostConfigOpen: false
};
function failedRequestHandler(e, Messages) {
Messages.error('Error', errorMsgFilter(e));
}
function rmEmptyKeys(col) {
for (var key in col) {
if (col[key] === null || col[key] === undefined || col[key] === '' || ($.isPlainObject(col[key]) && $.isEmptyObject(col[key])) || col[key].length === 0) {
delete col[key];
}
}
}
function getNames(arr) {
return arr.map(function (item) {
return item.name;
});
}
$scope.create = function () {
// Copy the config before transforming fields to the remote API format
$('#create-modal').modal('hide');
ViewSpinner.spin();
var config = angular.copy($scope.config);
if (config.Cmd && config.Cmd[0] === "[") {
config.Cmd = angular.fromJson(config.Cmd);
} else if (config.Cmd) {
config.Cmd = config.Cmd.split(' ');
}
config.Env = config.Env.map(function (envar) {
return envar.name + '=' + envar.value;
});
var labels = {};
config.Labels = config.Labels.forEach(function(label) {
labels[label.key] = label.value;
});
config.Labels = labels;
config.Volumes = getNames(config.Volumes);
config.SecurityOpts = getNames(config.SecurityOpts);
config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom);
config.HostConfig.Binds = getNames(config.HostConfig.Binds);
config.HostConfig.Links = getNames(config.HostConfig.Links);
config.HostConfig.Dns = getNames(config.HostConfig.Dns);
config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch);
config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd);
config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop);
config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) {
prev[cur.name] = cur.value;
return prev;
}, {});
config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) {
return entry.host + ':' + entry.ip;
});
var ExposedPorts = {};
var PortBindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
var intPort = portBinding.intPort + "/tcp";
if (portBinding.protocol === "udp") {
intPort = portBinding.intPort + "/udp";
}
var binding = {
HostIp: portBinding.ip,
HostPort: portBinding.extPort
};
if (portBinding.intPort) {
ExposedPorts[intPort] = {};
if (intPort in PortBindings) {
PortBindings[intPort].push(binding);
} else {
PortBindings[intPort] = [binding];
}
} else {
Messages.send('Warning', 'Internal port must be specified for PortBindings');
}
});
config.ExposedPorts = ExposedPorts;
config.HostConfig.PortBindings = PortBindings;
// Remove empty fields from the request to avoid overriding defaults
rmEmptyKeys(config.HostConfig);
rmEmptyKeys(config);
var ctor = Container;
var s = $scope;
Container.create(config, function (d) {
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
ctor.start(reqBody, function (cd) {
if (cd.id) {
ViewSpinner.stop();
Messages.send('Container Started', d.Id);
$state.go('container', {id: d.Id}, {reload: true});
} else {
ViewSpinner.stop();
failedRequestHandler(cd, Messages);
ctor.remove({id: d.Id}, function () {
Messages.send('Container Removed', d.Id);
});
}
}, function (e) {
ViewSpinner.stop();
failedRequestHandler(e, Messages);
});
} else {
ViewSpinner.stop();
failedRequestHandler(d, Messages);
}
}, function (e) {
ViewSpinner.stop();
failedRequestHandler(e, Messages);
});
};
$scope.addEntry = function (array, entry) {
array.push(entry);
};
$scope.rmEntry = function (array, entry) {
var idx = array.indexOf(entry);
array.splice(idx, 1);
};
}]);

View File

@@ -1,444 +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>Start a new container</h3>
</div>
<div class="modal-body">
<form role="form">
<accordion close-others="true">
<accordion-group heading="Container options" is-open="menuStatus.containerOpen">
<fieldset>
<div class="row">
<div class="col-xs-6">
<div class="form-group">
<label>Image:</label>
<input type="text" placeholder='ubuntu:latest'
ng-model="config.Image" class="form-control"/>
</div>
<div class="form-group">
<label>Cmd:</label>
<input type="text" placeholder='["/bin/echo", "Hello world"]'
ng-model="config.Cmd" class="form-control"/>
<small>Input commands as a raw string or JSON array</small>
</div>
<div class="form-group">
<label>Entrypoint:</label>
<input type="text" ng-model="config.Entrypoint" class="form-control"
placeholder="./entrypoint.sh"/>
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="config.name" class="form-control"/>
</div>
<div class="form-group">
<label>Hostname:</label>
<input type="text" ng-model="config.Hostname" class="form-control"/>
</div>
<div class="form-group">
<label>Domainname:</label>
<input type="text" ng-model="config.Domainname" class="form-control"/>
</div>
<div class="form-group">
<label>User:</label>
<input type="text" ng-model="config.User" 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>Volumes:</label>
<div ng-repeat="volume in config.Volumes">
<div class="form-group form-inline">
<input type="text" ng-model="volume.name" class="form-control"
placeholder="/var/data"/>
<a href="" ng-click="rmEntry(config.Volumes, volume)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.Volumes, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="col-xs-6">
<div class="form-group">
<label>MemorySwap:</label>
<input type="number" ng-model="config.MemorySwap" class="form-control"/>
</div>
<div class="form-group">
<label>CpuShares:</label>
<input type="number" ng-model="config.CpuShares" class="form-control"/>
</div>
<div class="form-group">
<label>Cpuset:</label>
<input type="text" ng-model="config.Cpuset" class="form-control"
placeholder="1,2"/>
<small>Input as comma-separated list of numbers</small>
</div>
<div class="form-group">
<label>WorkingDir:</label>
<input type="text" ng-model="config.WorkingDir" class="form-control"
placeholder="/app"/>
</div>
<div class="form-group">
<label>MacAddress:</label>
<input type="text" ng-model="config.MacAddress" class="form-control"
placeholder="12:34:56:78:9a:bc"/>
</div>
<div class="form-group">
<label for="networkDisabled">NetworkDisabled:</label>
<input id="networkDisabled" type="checkbox"
ng-model="config.NetworkDisabled"/>
</div>
<div class="form-group">
<label for="tty">Tty:</label>
<input id="tty" type="checkbox" ng-model="config.Tty"/>
</div>
<div class="form-group">
<label for="openStdin">OpenStdin:</label>
<input id="openStdin" type="checkbox" ng-model="config.OpenStdin"/>
</div>
<div class="form-group">
<label for="stdinOnce">StdinOnce:</label>
<input id="stdinOnce" type="checkbox" ng-model="config.StdinOnce"/>
</div>
<div class="form-group">
<label>SecurityOpts:</label>
<div ng-repeat="opt in config.SecurityOpts">
<div class="form-group form-inline">
<input type="text" ng-model="opt.name" class="form-control"
placeholder="label:type:svirt_apache"/>
<a href="" ng-click="rmEntry(config.SecurityOpts, opt)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.SecurityOpts, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<hr>
<div class="form-group">
<label>Env:</label>
<div ng-repeat="envar in config.Env">
<div class="form-group form-inline">
<div class="form-group">
<label class="sr-only">Variable Name:</label>
<input type="text" ng-model="envar.name" class="form-control"
placeholder="NAME"/>
</div>
<div class="form-group">
<label class="sr-only">Variable Value:</label>
<input type="text" ng-model="envar.value" class="form-control"
placeholder="value"/>
</div>
<div class="form-group">
<a href="" ng-click="rmEntry(config.Env, envar)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<a href="" ng-click="addEntry(config.Env, {name: '', value: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>Labels:</label>
<div ng-repeat="label in config.Labels">
<div class="form-group form-inline">
<div class="form-group">
<label class="sr-only">Key:</label>
<input type="text" ng-model="label.key" class="form-control"
placeholder="key"/>
</div>
<div class="form-group">
<label class="sr-only">Value:</label>
<input type="text" ng-model="label.value" class="form-control"
placeholder="value"/>
</div>
<div class="form-group">
<a href="" ng-click="rmEntry(config.Labels, label)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<a href="" ng-click="addEntry(config.Labels, {key: '', value: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
</fieldset>
</accordion-group>
<accordion-group heading="HostConfig options" is-open="menuStatus.hostConfigOpen">
<fieldset>
<div class="row">
<div class="col-xs-6">
<div class="form-group">
<label>Binds:</label>
<div ng-repeat="bind in config.HostConfig.Binds">
<div class="form-group form-inline">
<input type="text" ng-model="bind.name" class="form-control"
placeholder="/host:/container"/>
<a href="" ng-click="rmEntry(config.HostConfig.Binds, bind)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.Binds, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>Links:</label>
<div ng-repeat="link in config.HostConfig.Links">
<div class="form-group form-inline">
<input type="text" ng-model="link.name" class="form-control"
placeholder="web:db">
<a href="" ng-click="rmEntry(config.HostConfig.Links, link)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.Links, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>Dns:</label>
<div ng-repeat="entry in config.HostConfig.Dns">
<div class="form-group form-inline">
<input type="text" ng-model="entry.name" class="form-control"
placeholder="8.8.8.8"/>
<a href="" ng-click="rmEntry(config.HostConfig.Dns, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.Dns, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>DnsSearch:</label>
<div ng-repeat="entry in config.HostConfig.DnsSearch">
<div class="form-group form-inline">
<input type="text" ng-model="entry.name" class="form-control"
placeholder="example.com"/>
<a href="" ng-click="rmEntry(config.HostConfig.DnsSearch, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.DnsSearch, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>CapAdd:</label>
<div ng-repeat="entry in config.HostConfig.CapAdd">
<div class="form-group form-inline">
<input type="text" ng-model="entry.name" class="form-control"
placeholder="cap_sys_admin"/>
<a href="" ng-click="rmEntry(config.HostConfig.CapAdd, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.CapAdd, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>CapDrop:</label>
<div ng-repeat="entry in config.HostConfig.CapDrop">
<div class="form-group form-inline">
<input type="text" ng-model="entry.name" class="form-control"
placeholder="cap_sys_admin"/>
<a href="" ng-click="rmEntry(config.HostConfig.CapDrop, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.CapDrop, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="col-xs-6">
<div class="form-group">
<label>NetworkMode:</label>
<input type="text" ng-model="config.HostConfig.NetworkMode"
class="form-control" placeholder="bridge"/>
</div>
<div class="form-group">
<label for="publishAllPorts">PublishAllPorts:</label>
<input id="publishAllPorts" type="checkbox"
ng-model="config.HostConfig.PublishAllPorts"/>
</div>
<div class="form-group">
<label for="privileged">Privileged:</label>
<input id="privileged" type="checkbox"
ng-model="config.HostConfig.Privileged"/>
</div>
<div class="form-group">
<label>VolumesFrom:</label>
<div ng-repeat="volume in config.HostConfig.VolumesFrom">
<div class="form-group form-inline">
<select ng-model="volume.name"
ng-options="name for name in containerNames track by name"
class="form-control">
</select>
<a href="" ng-click="rmEntry(config.HostConfig.VolumesFrom, volume)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.VolumesFrom, {name: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>RestartPolicy:</label>
<select ng-model="config.HostConfig.RestartPolicy.name">
<option value="">disabled</option>
<option value="always">always</option>
<option value="on-failure">on-failure</option>
</select>
<label>MaximumRetryCount:</label>
<input type="number"
ng-model="config.HostConfig.RestartPolicy.MaximumRetryCount"/>
</div>
</div>
</div>
<hr>
<div class="form-group">
<label>ExtraHosts:</label>
<div ng-repeat="entry in config.HostConfig.ExtraHosts">
<div class="form-group form-inline">
<div class="form-group">
<label class="sr-only">Hostname:</label>
<input type="text" ng-model="entry.host" class="form-control"
placeholder="hostname"/>
</div>
<div class="form-group">
<label class="sr-only">IP Address:</label>
<input type="text" ng-model="entry.ip" class="form-control"
placeholder="127.0.0.1"/>
</div>
<div class="form-group">
<a href="" ng-click="rmEntry(config.HostConfig.ExtraHosts, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.ExtraHosts, {host: '', ip: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>LxcConf:</label>
<div ng-repeat="entry in config.HostConfig.LxcConf">
<div class="form-group form-inline">
<div class="form-group">
<label class="sr-only">Name:</label>
<input type="text" ng-model="entry.name" class="form-control"
placeholder="lxc.utsname"/>
</div>
<div class="form-group">
<label class="sr-only">Value:</label>
<input type="text" ng-model="entry.value" class="form-control"
placeholder="docker"/>
</div>
<div class="form-group">
<a href="" ng-click="rmEntry(config.HostConfig.LxcConf, entry)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.LxcConf, {name: '', value: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>Devices:</label>
<div ng-repeat="device in config.HostConfig.Devices">
<div class="form-group form-inline inline-four">
<label class="sr-only">PathOnHost:</label>
<input type="text" ng-model="device.PathOnHost" class="form-control"
placeholder="PathOnHost"/>
<label class="sr-only">PathInContainer:</label>
<input type="text" ng-model="device.PathInContainer" class="form-control"
placeholder="PathInContainer"/>
<label class="sr-only">CgroupPermissions:</label>
<input type="text" ng-model="device.CgroupPermissions" class="form-control"
placeholder="CgroupPermissions"/>
<a href="" ng-click="rmEntry(config.HostConfig.Devices, device)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.Devices, { PathOnHost: '', PathInContainer: '', CgroupPermissions: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
<div class="form-group">
<label>PortBindings:</label>
<div ng-repeat="portBinding in config.HostConfig.PortBindings">
<div class="form-group form-inline inline-four">
<label class="sr-only">Host IP:</label>
<input type="text" ng-model="portBinding.ip" class="form-control"
placeholder="Host IP Address"/>
<label class="sr-only">Host Port:</label>
<input type="text" ng-model="portBinding.extPort" class="form-control"
placeholder="Host Port"/>
<label class="sr-only">Container port:</label>
<input type="text" ng-model="portBinding.intPort" class="form-control"
placeholder="Container Port"/>
<select ng-model="portBinding.protocol">
<option value="">tcp</option>
<option value="udp">udp</option>
</select>
<a href="" ng-click="rmEntry(config.HostConfig.PortBindings, portBinding)">
<i class="fa fa-minus" aria-hidden="true"></i>
</a>
</div>
</div>
<a href="" ng-click="addEntry(config.HostConfig.PortBindings, {ip: '', extPort: '', intPort: ''})">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
</div>
</fieldset>
</accordion-group>
</accordion>
</form>
</div>
<div class="modal-footer">
<a href="" class="btn btn-primary btn-lg" ng-click="create()">Create</a>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
</rd-header-content>
</rd-header>
@@ -10,7 +10,7 @@
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
@@ -54,20 +54,40 @@
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes"></rd-widget-header>
<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">{{title}}</th>
<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 ng-repeat="processInfos in containerTop.Processes">
<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>

View File

@@ -1,9 +1,20 @@
angular.module('stats', [])
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
.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
@@ -87,7 +98,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
},
{
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10));
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
//scaleOverride: true,
@@ -100,19 +111,25 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
datasets: [networkRxDataset, networkTxDataset]
}, {
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10));
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?');
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
return;
}
@@ -121,15 +138,17 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
updateCpuChart(d);
updateMemoryChart(d);
updateNetworkChart(d);
timeout = $timeout(updateStats, 5000);
setUpdateStatsTimeout();
}, function () {
Messages.error('Unable to retrieve stats', 'Is this container running?');
timeout = $timeout(updateStats, 5000);
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);
});
@@ -154,16 +173,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
$scope.networkName = Object.keys(data.networks)[0];
data.network = data.networks[$scope.networkName];
}
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;
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();
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
function calculateCPUPercent(stats) {
@@ -188,7 +209,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to retrieve container info");
});
$scope.getTop();
}]);

View File

@@ -8,43 +8,7 @@
</rd-header>
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.Version }}</div>
<div class="comment">Swarm version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.ApiVersion }}</div>
<div class="comment">API version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.GoVersion }}</div>
<div class="comment">Go version</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<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">
@@ -52,84 +16,46 @@
<tbody>
<tr>
<td>Nodes</td>
<td>{{ swarm.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>
<td>Containers</td>
<td>{{ info.Containers }}</td>
</tr>
<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>CPUs</td>
<td>{{ info.NCPU }}</td>
<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>{{ info.MemTotal|humansize }}</td>
<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>
<td>Operating System</td>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Kernel Version</td>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Nodes status"></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('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('Containers')">
Containers
<span ng-show="sortType == 'Containers' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Containers' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('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 ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
<td>{{ node.name }}</td>
<td>{{ node.ip }}</td>
<td>{{ node.containers }}</td>
<td><span class="label label-{{ node.status|statusbadge }}">{{ node.status }}</span></td>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
</tbody>
</table>
@@ -137,3 +63,164 @@
</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>

View File

@@ -1,62 +1,85 @@
angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings',
function ($scope, Info, Version, Settings) {
.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.sortType = 'Name';
$scope.sortReverse = true;
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$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;
extractSwarmInfo(d);
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 node_count = parseInt(systemStatus[3][1], 10);
$scope.swarm[systemStatus[3][0]] = node_count;
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);
}
$scope.swarm.Status = [];
extractNodesInfo(systemStatus, node_count);
}
function extractNodesInfo(info, node_count) {
// First information for node1 available at element #4 of SystemStatus
// The next 10 elements are information related to the node
var node_offset = 4;
for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset);
node_offset += 9;
}
}
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.status = info[offset + 1][1];
node.containers = info[offset + 2][1];
node.cpu = info[offset + 3][1];
node.memory = info[offset + 4][1];
node.labels = info[offset + 5][1];
node.error = info[offset + 6][1];
node.version = info[offset + 8][1];
$scope.swarm.Status.push(node);
}
}]);
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);
}
}]);

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>

View File

@@ -0,0 +1,29 @@
angular.module('task', [])
.controller('TaskController', ['$scope', '$stateParams', '$state', 'Task', 'Service', 'Messages',
function ($scope, $stateParams, $state, Task, Service, Messages) {
$scope.task = {};
$scope.serviceName = 'service';
$scope.isTaskRunning = false;
function fetchTaskDetails() {
$('#loadingViewSpinner').show();
Task.get({id: $stateParams.id}, function (d) {
$scope.task = d;
fetchAssociatedServiceDetails(d.ServiceID);
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve task details");
});
}
function fetchAssociatedServiceDetails(serviceId) {
Service.get({id: serviceId}, function (d) {
$scope.serviceName = d.Spec.Name;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve associated service details");
});
}
fetchTaskDetails();
}]);

View File

@@ -0,0 +1,145 @@
<rd-header>
<rd-header-title title="Application templates list">
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div id="selectedTemplate" class="row" ng-if="state.selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.logo" title="state.selectedTemplate.image">
</rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
</div>
</div>
<!-- name-and-network-inputs -->
<div class="form-group">
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
</div>
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
<div class="col-sm-4">
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !name-and-network-inputs -->
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10">
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
</a>
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
<div class="form-group" ng-if="state.showAdvancedOptions">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11" style="margin-top: 5px;">
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional 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.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 -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
<i id="createContainerSpinner" 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-rocket" title="Available templates">
<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="padding">
<div class="template-list">
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
<img class="logo" ng-src="{{ tpl.logo }}" />
<div class="title">{{ tpl.title }}</div>
<div class="description">{{ tpl.description }}</div>
</div>
<div ng-if="!templates" class="text-center text-muted">
Loading...
</div>
<div ng-if="templates.length == 0" class="text-center text-muted">
No templates available.
</div>
</div>
<div ng-if="templates">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,241 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination',
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
pagination_count: Pagination.getPaginationCount('templates')
};
$scope.formValues = {
network: "",
name: "",
ports: []
};
var selectedItem = -1;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
};
$scope.addPortBinding = function() {
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.formValues.ports.splice(index, 1);
};
// TODO: centralize, already present in createContainerController
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 createContainerController
function pullImageAndCreateContainer(imageConfig, containerConfig) {
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error("Error", {}, detail.error);
} else {
createContainer(containerConfig);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
});
}
function getInitialConfiguration() {
return {
Env: [],
OpenStdin: false,
Tty: false,
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: {},
Binds: [],
NetworkMode: $scope.formValues.network.Name,
Privileged: false
},
Volumes: {},
name: $scope.formValues.name
};
}
function preparePortBindings(config, ports) {
var bindings = {};
ports.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 createConfigFromTemplate(template) {
var containerConfig = getInitialConfiguration();
containerConfig.Image = template.image;
if (template.env) {
template.env.forEach(function (v) {
if (v.value || v.set) {
var val;
if (v.type && v.type === 'container') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
val = $filter('swarmcontainername')(v.value);
} else {
var container = v.value;
val = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
}
} else {
val = v.set ? v.set : v.value;
}
containerConfig.Env.push(v.name + "=" + val);
}
});
}
preparePortBindings(containerConfig, $scope.formValues.ports);
prepareImageConfig(containerConfig, template);
return containerConfig;
}
function prepareImageConfig(config, template) {
var image = template.image;
var registry = template.registry || '';
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
function prepareVolumeQueries(template, containerConfig) {
var volumeQueries = [];
if (template.volumes) {
template.volumes.forEach(function (vol) {
volumeQueries.push(
Volume.create({}, function (d) {
if (d.message) {
Messages.error("Unable to create volume", {}, d.message);
} else {
Messages.send("Volume created", d.Name);
containerConfig.Volumes[vol] = {};
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
}
}, function (e) {
Messages.error("Failure", e, "Unable to create volume");
}).$promise
);
});
}
return volumeQueries;
}
$scope.createTemplate = function() {
$('#createContainerSpinner').show();
var template = $scope.state.selectedTemplate;
var containerConfig = createConfigFromTemplate(template);
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
$q.all(createVolumeQueries).then(function (d) {
pullImageAndCreateContainer($scope.imageConfig, containerConfig);
});
};
$scope.selectTemplate = function(id) {
$('#template_' + id).toggleClass("container-template--selected");
if (selectedItem === id) {
selectedItem = -1;
$scope.state.selectedTemplate = null;
} else {
$('#template_' + selectedItem).toggleClass("container-template--selected");
selectedItem = id;
var selectedTemplate = $scope.templates[id];
$scope.state.selectedTemplate = selectedTemplate;
$scope.formValues.ports = selectedTemplate.ports ? TemplateHelper.getPortBindings(selectedTemplate.ports) : [];
$anchorScroll('selectedTemplate');
}
};
function initTemplates() {
Templates.get(function (data) {
$scope.templates = data.map(function(tpl,index){
tpl.index = index;
return tpl;
});
$('#loadTemplatesSpinner').hide();
}, function (e) {
$('#loadTemplatesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve apps list");
$scope.templates = [];
});
}
Config.$promise.then(function (c) {
var containersToHideLabels = c.hiddenLabels;
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({Scope: "local", Name: "bridge"});
networks.push({Scope: "local", Name: "host"});
networks.push({Scope: "local", Name: "none"});
} else {
$scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; });
}
$scope.availableNetworks = networks;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
Container.query({all: 0}, 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");
});
initTemplates();
});
}]);

View File

@@ -1,10 +1,9 @@
<div ng-include="template" ng-controller="CreateVolumeController"></div>
<rd-header>
<rd-header-title title="Volume list">
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadVolumesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Volumes</rd-header-content>
</rd-header>
@@ -12,13 +11,21 @@
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
<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-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#create-volume-modal">Create new volume...</button>
</div>
<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" type="button" ui-sref="actions.create.volume">Add volume</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -26,10 +33,12 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="volumes" ng-click="order('Name')">
Name
@@ -54,15 +63,24 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse))">
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:20 }}</td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
<tr ng-if="!volumes">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="volumes.length == 0">
<td colspan="4" class="text-center text-muted">No volumes available.</td>
</tr>
</tbody>
</table>
<div ng-if="volumes" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
<rd-widget>
</div>

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