Compare commits

...

93 Commits

Author SHA1 Message Date
Anthony Lapenna
3ff49542f3 Merge branch 'release/1.16.2' 2018-02-08 09:27:20 +01:00
Anthony Lapenna
27dcfd043b chore(version): bump version number 2018-02-08 09:27:13 +01:00
Anthony Lapenna
1de0619fd5 fix(api): ignore Docker login errors during stack deployment (#1635) 2018-02-07 08:37:01 +01:00
Anthony Lapenna
1c67db0c70 feat(ux): enable auto-focus on search field (#1636) 2018-02-06 16:58:05 +01:00
Anthony Lapenna
7365e69c59 fix(config-creation): fix an issue setting config editor as read-only (#1634) 2018-02-06 14:23:08 +01:00
Anthony Lapenna
23a565243a Merge branch 'develop' of github.com:portainer/portainer into develop 2018-02-01 13:29:43 +01:00
Anthony Lapenna
27dceadba1 refactor(app): introduce new project structure for the frontend (#1623) 2018-02-01 13:27:52 +01:00
Anthony Lapenna
6f471cef34 Merge branch 'master' into develop 2018-01-31 21:35:20 +01:00
Ben Yanke
e6422a6d75 style(container-details): fix a typo in container status 2018-01-31 20:28:36 +01:00
Anthony Lapenna
56cab429de Revert "feat(container-details): fix typo in container status" (#1619)
This reverts commit 5f742c2163.
2018-01-31 19:11:20 +01:00
Ben Yanke
5f742c2163 feat(container-details): fix typo in container status 2018-01-31 19:09:10 +01:00
Anthony Lapenna
f31f29fa2f feat(volumes): check if volumes are used in service definitions (#1601) 2018-01-25 08:13:56 +01:00
Anthony Lapenna
672819f3af refactor(api): remove CLI deprecation related code (#1602) 2018-01-24 21:58:58 +01:00
Anthony Lapenna
0ff0c3ed0d Merge tag '1.16.1' into develop
Release 1.16.1
2018-01-23 16:53:03 +01:00
Anthony Lapenna
54750f002a Merge branch 'release/1.16.1' 2018-01-23 16:52:59 +01:00
Anthony Lapenna
4c2dfb3346 chore(version): bump version number 2018-01-23 16:52:54 +01:00
Miguel A. C
8ae3abf29e fix(service-details): avoid sending unmodified restart policy settings when updating a service (#1576) 2018-01-23 10:06:58 +01:00
Anthony Lapenna
362f036a68 fix(state): ensure API version >= 1.25 before extension check (#1594)
* fix(state): ensure API version >= 1.25 before extension check
2018-01-23 09:50:14 +01:00
Anthony Lapenna
0d0072a50e extension(storidge): support cluster shutdown (#1589) 2018-01-23 09:49:29 +01:00
Anthony Lapenna
173ea372c2 fix(extension): bypass the error returned by plugin service during ex… (#1586)
* fix(extension): bypass the error returned by plugin service during extension check

* feat(plugins): bypass the error returned by plugin service during plugin retrieval
2018-01-23 09:47:36 +01:00
Anthony Lapenna
8c75f705e2 chore(dependency): upgrade jquery version to latest (#1592) 2018-01-22 17:44:49 +01:00
Anthony Lapenna
b1863430df revert: revert PR 1366 (#1588) 2018-01-22 10:06:47 +01:00
Anthony Lapenna
c51db23c32 Merge tag '1.16.0' into develop
Release 1.16.0
2018-01-21 17:30:18 +01:00
Anthony Lapenna
c40f120da2 Merge branch 'release/1.16.0' 2018-01-21 17:30:13 +01:00
Anthony Lapenna
a7cb0ca823 chore(version): bump version number 2018-01-21 17:30:06 +01:00
Anthony Lapenna
7817d4bd0b extension(storidge): add Storidge extension (#1581) 2018-01-21 17:26:24 +01:00
Miguel A. C
edadce359c feat(stack-details): add stack deploy prune option (#1567)
* feat(stack-details): add stack deploy prune option

* fix go fmt issues

* add changes proposed by reviewer

* refactor deployStack as suggested by codeclimate
2018-01-20 18:05:01 +01:00
Anthony Lapenna
e1bf9599ef fix(stack-details): fix broken link for services published ports (#1578) 2018-01-20 11:31:26 +01:00
RobbyVoid
c3ba9e6a53 feat(networks): Show untruncated network name as link title (#1574)
If the network name was truncated (40 characters) it should be visible as a mouse over title
2018-01-19 12:41:18 +01:00
Vincent Besançon
10174b98b9 refactor(api): Fixed typo in check health cli flag (#1570) 2018-01-17 16:34:15 +01:00
1138-4EB
6acfb580dc feat(cli): Add CLI flag for health-check (#1366) 2018-01-15 19:34:07 +01:00
Miguel A. C
340ec841fe feat(swarm-visualizer): add auto-refresh to the cluster visualizer (#1561) 2018-01-12 16:10:02 +01:00
Anthony Lapenna
a515b96a46 fix(app): fix a Javascript error related to missing $state parameter (#1562) 2018-01-09 20:06:19 +01:00
Anthony Lapenna
46da85c8cf feat(services): bind enter key when scaling a service (#1560) 2018-01-09 10:59:33 +01:00
Anthony Lapenna
f52ac8fb12 feat(UX): improve UX for service update (#1558) 2018-01-09 10:40:30 +01:00
Miguel A. C
0e28aebd65 feat(service): add force update in service list/detail (#1536) 2018-01-08 22:06:56 +01:00
Anthony Lapenna
35892525ff docs(api): document the stack management endpoint (#1557) 2018-01-08 18:27:45 +01:00
Anthony Lapenna
d2f3309842 refactor(api): rename file package to filesystem (#1555) 2018-01-06 18:53:12 +01:00
Rahul Ruikar
03f6cc0acf feat(templates): add labels to container template (#1538) 2018-01-06 18:24:51 +01:00
cbrherms
f8c7ee7ae6 feat(container-creation): add support for mac assignments (#1546)
* feat(container-creation): add support for mac assignments (#1524)

* refactor(container-creation): code relocation to relevant function

* style(container-creation): fix typo in environment variables function
2018-01-06 11:53:03 +01:00
Thomas Krzero
00daedca30 fix(service): check endpoint spec existence before update 2018-01-05 14:49:41 +01:00
Anthony Lapenna
e2b8633aac fix(stack-details): fix an issue related to env vars (#1512) 2018-01-05 14:32:23 +01:00
Anthony Lapenna
50dbb572b1 fix(containers): update the persisted filters after refresh (#1553) 2018-01-05 14:31:20 +01:00
Anthony Lapenna
95b595d2a9 fix(UAC): fix an issue with network/volume ownership update (#1552) 2018-01-05 13:43:25 +01:00
Anthony Lapenna
f57ce8b327 feat(containers): trim the @sha256 suffix in the image name (#1551) 2018-01-05 12:32:53 +01:00
Anthony Lapenna
5787df5599 refactor(stack): replace $stateParams usage with $transition$.params() 2018-01-04 21:53:10 +01:00
Anthony Lapenna
52ac9504c1 chore(codefresh): fix the build_frontend step (#1547) 2018-01-02 13:14:26 +01:00
Yassir Hannoun
1da64f2e75 * fix(containers): display a subset of the sha images name in the containers datatable
* Removed unnecessary filter

* refactor(common): improve trimshasum  filter

* refactor(common): improve trimshasum filter
2017-12-22 19:39:06 +01:00
Miguel A. C
8bf3f669d0 feat(service): add logging driver config in service create/update (#1516) 2017-12-22 10:05:31 +01:00
Anthony Lapenna
eec10541b3 fix(users): fix invalid Authentication value (#1528) 2017-12-21 19:56:54 +01:00
Anthony Lapenna
e0b09f20b0 fix(cache): add a cache validity mechanism (#1527) 2017-12-21 19:49:39 +01:00
Miguel A. C
8e40eb1844 feat(service): add hosts file entries in service create/update (#1511) 2017-12-21 09:53:34 +01:00
Anthony Lapenna
c9e060d574 fix(container-logs): add missing dependency to Notifications (#1514) 2017-12-18 21:24:51 +01:00
Anthony Lapenna
9c9e16b2b2 fix(containers): fix the ability to stop/pause a healthy container (#1507) 2017-12-14 10:31:16 +01:00
Anthony Lapenna
35f7ce5f3d Merge tag '1.15.5' into develop
Release 1.15.5
2017-12-11 16:04:03 +01:00
Anthony Lapenna
45e7938c5c Merge branch 'release/1.15.5' 2017-12-11 16:03:58 +01:00
Anthony Lapenna
fbd9139928 chore(version): bump version number 2017-12-11 16:03:53 +01:00
Anthony Lapenna
d0da9860af style(datatables): use normal font weight for table headers (#1496) 2017-12-11 16:03:00 +01:00
Duvel
46d8dba137 style(networks): change the label of the add button (#1495) 2017-12-11 15:50:59 +01:00
Anthony Lapenna
3660f6eeb5 Merge tag '1.15.4' into develop
Release 1.15.4
2017-12-10 10:10:01 +01:00
Anthony Lapenna
39236ae84e Merge branch 'release/1.15.4' 2017-12-10 10:09:56 +01:00
Anthony Lapenna
7dcf5c2d0b chore(version): bump version number 2017-12-10 10:09:11 +01:00
Miguel A. C
d0e147137d feat(service): add restart policy options in service create/details (#1479) 2017-12-07 21:05:45 +01:00
Anthony Lapenna
bdb23a8dd2 feat(UX): replace tables with datatables (#1460) 2017-12-06 12:04:02 +01:00
1138-4EB
7922ecc4a1 chore(build-system): refactor gruntfile (#1447) 2017-12-05 21:26:45 +01:00
Miguel A. C
728ef35cc1 feat(service): change update delay format to a time string in service… (#1470) 2017-12-05 20:12:54 +01:00
Anthony Lapenna
f3a23c7dd1 feat(container-details): display loading when using recreate (#1471) 2017-12-05 17:46:11 +01:00
Anthony Lapenna
283faca4f7 feat(dashboard): add a link to the visualizer (#1469) 2017-12-05 17:34:29 +01:00
Anthony Lapenna
2b2850d17a fix(stacks): fix an issue with stacks using docker in their name (#1468) 2017-12-05 14:56:40 +01:00
1138-4EB
997af882c4 chore(build-system): drop bower, use npm|yarn for frontend dependencies (#1416)
* chore(build-system): drop bower, use npm|yarn for frontend dependencies

* chore(build-sytem): for github dependencies, use semver format instead of tag/commit

* add yarn.lock
2017-12-05 09:52:38 +01:00
1138-4EB
75b3a78e2b refactor(services): Refactor chartService and pluginService (#1340) 2017-12-05 09:49:04 +01:00
Miguel A. C
d8f6b14726 feat(authentication-settings): add default port when not set in url (#1456) 2017-12-04 19:41:59 +01:00
Miguel A. C
406757d751 feat(swarm-visualizer): add ram and cpu info to nodes & limits to tasks (#1458) 2017-12-04 18:01:07 +01:00
Miguel A. C
f3b5f803f5 feat(tasks): add missing task states, set new default state color (#1459) 2017-12-04 17:58:46 +01:00
1138-4EB
f1d9b72a06 docs(README): add ref to portainer-demo/play-with-docker (#1455) 2017-12-02 09:39:10 +01:00
doncicuto
9513da80f6 feat(node): add engine labels info in the swarm nodes view (#1451) 2017-12-01 09:26:03 +01:00
Anthony Lapenna
ca036b56c1 feat(database-migration): enable donation header when upgrading Portainer (#1450) 2017-11-28 13:40:33 +01:00
Anthony Lapenna
27a388a030 Merge tag '1.15.3' into develop
Release 1.15.3
2017-11-26 10:08:08 +01:00
Anthony Lapenna
65cde27334 Merge branch 'release/1.15.3' 2017-11-26 10:08:04 +01:00
Anthony Lapenna
2275467bdc chore(version): bump version number 2017-11-26 10:07:59 +01:00
1138-4EB
688b15fb4b feat(about): add a new about view as well as a support header 2017-11-26 10:05:03 +01:00
Anthony Lapenna
3362ba0c8c fix(services): do not display exposed ports when published port is missing (#1440) 2017-11-25 10:30:40 +01:00
Anthony Lapenna
39cf4d75ff fix(container-creation): reset NetworkConfig when changing the network during container edition (#1431) 2017-11-23 16:02:40 +01:00
Duvel
13d8d38bf9 fix(service-details): fix an issue with invalid service restart policy (#1415) 2017-11-23 10:47:39 +01:00
1138-4EB
e51246ee78 style(sidebar): prevent icon of active item moving on hover (#1422) 2017-11-23 09:59:29 +01:00
Anthony Lapenna
4ab580923f fix(templates): fix an issue preventing linuxserver.io templates to be displayed (#1426) 2017-11-22 22:16:53 +01:00
1138-4EB
547511c8aa feat(UX): change background color for selected items (#1414) 2017-11-20 17:46:01 +01:00
1138-4EB
8a101f67f6 style(container-details): change the grouping of buttons
* style(containers) make add container button responsive

* style(container) make action buttons responsive, group as in containers
2017-11-20 14:48:42 +01:00
Thomas Krzero
3ee2e20f8e feat(services): add the ability to specify a target for secrets (#1365) 2017-11-20 14:44:23 +01:00
Yassir Hannoun
6b9f3dad7a feat(UX): add an image autocomplete feature for services and containers (#1389) 2017-11-20 14:34:14 +01:00
1138-4EB
a2d41e5316 feat(build-system): check that files listed in vendor.yml exist (#1398)
* chore(build-system) check that files listed in vendor.yml exist (#1410)

* fix(build-system) Chart.min.js duplicated in vendor.yml (#1410)
2017-11-20 10:09:11 +01:00
Thomas Kooi
3548f0db6f refactor(webapp): simplify isAdmin statement (#1388) 2017-11-14 08:54:35 +01:00
Anthony Lapenna
521cc3d6ab Merge tag '1.15.2' into develop
Release 1.15.2
2017-11-13 10:11:27 +01:00
436 changed files with 15549 additions and 5944 deletions

View File

@@ -26,6 +26,14 @@ You can try out the public demo instance: http://demo.portainer.io/ (login with
Please note that the public demo cluster is **reset every 15min**.
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
## Getting started
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)

View File

@@ -0,0 +1,16 @@
package bolt
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}

View File

@@ -81,6 +81,14 @@ func (m *Migrator) Migrate() error {
}
}
// https://github.com/portainer/portainer/issues/1449
if m.CurrentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err

View File

@@ -1,7 +1,6 @@
package cli
import (
"log"
"time"
"github.com/portainer/portainer"
@@ -31,12 +30,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
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(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
@@ -46,12 +44,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
// Deprecated flags
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
}
kingpin.Parse()
@@ -97,8 +95,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return errAdminPassExcludeAdminPassFile
}
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
return nil
}
@@ -142,15 +138,3 @@ func validateSyncInterval(syncInterval string) error {
}
return nil
}
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
if templates != "" {
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
}
if logo != "" {
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
}
if labels != nil {
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/portainer/portainer/cron"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/exec"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
"github.com/portainer/portainer/git"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/jwt"
@@ -31,7 +31,7 @@ func initCLI() *portainer.CLIFlags {
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := file.NewService(dataStorePath, "")
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
@@ -127,6 +127,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
if err == portainer.ErrSettingsNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
DisplayDonationHeader: true,
DisplayExternalContributors: false,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{

View File

@@ -23,27 +23,19 @@ func NewStackManager(binaryPath string) *StackManager {
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) error {
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
err := runCommandAndCaptureStdErr(command, registryArgs, nil)
if err != nil {
return err
}
runCommandAndCaptureStdErr(command, registryArgs, nil)
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
err := runCommandAndCaptureStdErr(command, dockerhubArgs, nil)
if err != nil {
return err
}
runCommandAndCaptureStdErr(command, dockerhubArgs, nil)
}
return nil
}
// Logout executes the docker logout command.
@@ -54,10 +46,15 @@ func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
}
// Deploy executes the docker stack deploy command.
func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
} else {
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
}
env := make([]string, 0)
for _, envvar := range stack.Env {

View File

@@ -1,4 +1,4 @@
package file
package filesystem
import (
"bytes"

View File

@@ -48,7 +48,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
if strings.Contains(r.URL.Path, "/docker") {
if strings.Contains(r.URL.Path, "/docker/") {
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
} else if strings.Contains(r.URL.Path, "/stacks") {
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)

View File

@@ -5,7 +5,7 @@ import (
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
@@ -46,6 +46,7 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
type (
publicSettingsResponse struct {
LogoURL string `json:"LogoURL"`
DisplayDonationHeader bool `json:"DisplayDonationHeader"`
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
@@ -56,6 +57,7 @@ type (
TemplatesURL string `valid:"required"`
LogoURL string `valid:""`
BlackListedLabels []portainer.Pair `valid:""`
DisplayDonationHeader bool `valid:""`
DisplayExternalContributors bool `valid:""`
AuthenticationMethod int `valid:"required"`
LDAPSettings portainer.LDAPSettings `valid:""`
@@ -90,6 +92,7 @@ func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
DisplayDonationHeader: settings.DisplayDonationHeader,
DisplayExternalContributors: settings.DisplayExternalContributors,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
@@ -118,6 +121,7 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
TemplatesURL: req.TemplatesURL,
LogoURL: req.LogoURL,
BlackListedLabels: req.BlackListedLabels,
DisplayDonationHeader: req.DisplayDonationHeader,
DisplayExternalContributors: req.DisplayExternalContributors,
LDAPSettings: req.LDAPSettings,
AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers,
@@ -134,11 +138,11 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
}
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
} else {
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
err := handler.FileService.DeleteTLSFiles(file.LDAPStorePath)
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
@@ -165,7 +169,7 @@ func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter
}
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
@@ -37,6 +37,14 @@ type StackHandler struct {
StackManager portainer.StackManager
}
type stackDeploymentConfig struct {
endpoint *portainer.Endpoint
stack *portainer.Stack
prune bool
dockerhub *portainer.DockerHub
registries []portainer.Registry
}
// NewStackHandler returns a new instance of StackHandler.
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
h := &StackHandler{
@@ -78,6 +86,7 @@ type (
putStackRequest struct {
StackFileContent string `valid:"required"`
Env []portainer.Pair `valid:""`
Prune bool `valid:"-"`
}
)
@@ -166,7 +175,7 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
ID: portainer.StackID(stackName + "_" + swarmID),
Name: stackName,
SwarmID: swarmID,
EntryPoint: file.ComposeFileDefaultName,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: req.Env,
}
@@ -207,7 +216,14 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -264,7 +280,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
}
if req.PathInRepository == "" {
req.PathInRepository = file.ComposeFileDefaultName
req.PathInRepository = filesystem.ComposeFileDefaultName
}
stacks, err := handler.StackService.Stacks()
@@ -334,7 +350,14 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -404,7 +427,7 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
ID: portainer.StackID(stackName + "_" + swarmID),
Name: stackName,
SwarmID: swarmID,
EntryPoint: file.ComposeFileDefaultName,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: env,
}
@@ -445,7 +468,14 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -637,7 +667,14 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: req.Prune,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -732,22 +769,18 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re
}
}
func (handler *StackHandler) deployStack(endpoint *portainer.Endpoint, stack *portainer.Stack, dockerhub *portainer.DockerHub, registries []portainer.Registry) error {
func (handler *StackHandler) deployStack(config *stackDeploymentConfig) error {
handler.stackCreationMutex.Lock()
err := handler.StackManager.Login(dockerhub, registries, endpoint)
handler.StackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.StackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err
}
err = handler.StackManager.Deploy(stack, endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err
}
err = handler.StackManager.Logout(endpoint)
err = handler.StackManager.Logout(config.endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err

View File

@@ -12,13 +12,17 @@ type (
// CLIFlags represents the available flags on the CLI.
CLIFlags struct {
Addr *string
AdminPassword *string
AdminPasswordFile *string
Assets *string
Data *string
ExternalEndpoints *string
SyncInterval *string
Endpoint *string
ExternalEndpoints *string
Labels *[]Pair
Logo *string
NoAuth *bool
NoAnalytics *bool
Templates *string
TLSVerify *bool
TLSCacert *string
TLSCert *string
@@ -26,12 +30,7 @@ type (
SSL *bool
SSLCert *string
SSLKey *string
AdminPassword *string
AdminPasswordFile *string
// Deprecated fields
Logo *string
Templates *string
Labels *[]Pair
SyncInterval *string
}
// Status represents the application status.
@@ -73,6 +72,7 @@ type (
TemplatesURL string `json:"TemplatesURL"`
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
DisplayDonationHeader bool `json:"DisplayDonationHeader"`
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
@@ -380,18 +380,18 @@ type (
// StackManager represents a service to manage stacks.
StackManager interface {
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) error
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint)
Logout(endpoint *Endpoint) error
Deploy(stack *Stack, endpoint *Endpoint) error
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
Remove(stack *Stack, endpoint *Endpoint) error
}
)
const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.15.2"
APIVersion = "1.16.2"
// DBVersion is the version number of the Portainer database.
DBVersion = 6
DBVersion = 7
// DefaultTemplatesURL represents the default URL for the templates definitions.
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
)

View File

@@ -56,7 +56,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.15.2"
version: "1.16.2"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -77,6 +77,8 @@ tags:
description: "Manage Portainer settings"
- name: "status"
description: "Information about the Portainer instance"
- name: "stacks"
description: "Manage Docker stacks"
- name: "users"
description: "Manage users"
- name: "teams"
@@ -436,6 +438,285 @@ paths:
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks:
get:
tags:
- "stacks"
summary: "List stacks"
description: |
List all stacks based on the current user authorizations.
Will return all stacks if using an administrator account otherwise it
will only return the list of stacks the user have access to.
**Access policy**: restricted
operationId: "StackList"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackListResponse"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Access denied to resource"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
post:
tags:
- "stacks"
summary: "Deploy a new stack"
description: |
Deploy a new stack into a Docker environment specified via the endpoint identifier.
**Access policy**: restricted
operationId: "StackCreate"
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "method"
in: "query"
description: "Stack deployment method. Possible values: string or repository."
required: true
type: "string"
- in: "body"
name: "body"
description: "Stack details. Used when"
required: true
schema:
$ref: "#/definitions/StackCreateRequest"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackCreateResponse"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request data format"
404:
description: "Endpoint not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks/{id}:
get:
tags:
- "stacks"
summary: "Inspect a stack"
description: |
Retrieve details about a stack.
**Access policy**: restricted
operationId: "StackInspect"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/Stack"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
put:
tags:
- "stacks"
summary: "Update a stack"
description: |
Update a stack.
**Access policy**: restricted
operationId: "StackUpdate"
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
- in: "body"
name: "body"
description: "Stack details"
required: true
schema:
$ref: "#/definitions/StackUpdateRequest"
responses:
200:
description: "Success"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request data format"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
delete:
tags:
- "stacks"
summary: "Remove a stack"
description: |
Remove a stack.
**Access policy**: restricted
operationId: "StackDelete"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks/{id}/stackfile:
get:
tags:
- "stacks"
summary: "Retrieve the content of the Stack file for the specified stack"
description: |
Get Stack file content.
**Access policy**: restricted
operationId: "StackFileInspect"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackFileInspectResponse"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/registries:
get:
tags:
@@ -536,7 +817,7 @@ paths:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint not found"
err: "Registry not found"
500:
description: "Server error"
schema:
@@ -593,13 +874,6 @@ paths:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
503:
description: "Endpoint management disabled"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint management is disabled"
delete:
tags:
- "registries"
@@ -1869,7 +2143,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.15.2"
example: "1.16.2"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@@ -1880,6 +2154,10 @@ definitions:
description: "URL to a logo that will be displayed on the login page as well\
\ as on top of the sidebar. Will use default Portainer logo when value is\
\ empty string"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors:
type: "boolean"
example: false
@@ -1983,6 +2261,10 @@ definitions:
\ when querying containers"
items:
$ref: "#/definitions/Settings_BlackListedLabels"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors:
type: "boolean"
example: false
@@ -2398,6 +2680,10 @@ definitions:
\ when querying containers"
items:
$ref: "#/definitions/Settings_BlackListedLabels"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors:
type: "boolean"
example: false
@@ -2600,3 +2886,104 @@ definitions:
type: "string"
example: "nginx:latest"
description: "The Docker image associated to the template"
StackCreateRequest:
type: "object"
required:
- "Name"
- "SwarmID"
properties:
Name:
type: "string"
example: "myStack"
description: "Name of the stack"
SwarmID:
type: "string"
example: "jpofkc0i9uo9wtx1zesuk649w"
description: "Cluster identifier of the Swarm cluster"
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "Content of the Stack file. Required when using the 'string' deployment method."
GitRepository:
type: "string"
example: "https://github.com/openfaas/faas"
description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method."
PathInRepository:
type: "string"
example: "docker-compose.yml"
description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method."
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
Stack_Env:
properties:
name:
type: "string"
example: "MYSQL_ROOT_PASSWORD"
value:
type: "string"
example: "password"
StackCreateResponse:
type: "object"
properties:
Id:
type: "string"
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Id of the stack"
StackListResponse:
type: "array"
items:
$ref: "#/definitions/Stack"
Stack:
type: "object"
properties:
Id:
type: "string"
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Stack identifier"
Name:
type: "string"
example: "myStack"
description: "Stack name"
EntryPoint:
type: "string"
example: "docker-compose.yml"
description: "Path to the Stack file"
SwarmID:
type: "string"
example: "jpofkc0i9uo9wtx1zesuk649w"
description: "Cluster identifier of the Swarm cluster where the stack is deployed"
ProjectPath:
type: "string"
example: "/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Path on disk to the repository hosting the Stack file"
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
StackUpdateRequest:
type: "object"
properties:
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "New content of the Stack file."
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
Prune:
type: "boolean"
example: false
description: "Prune services that are no longer referenced"
StackFileInspectResponse:
type: "object"
properties:
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "Content of the Stack file."

View File

@@ -5,69 +5,16 @@ angular.module('portainer', [
'ngCookies',
'ngSanitize',
'ngFileUpload',
'ngMessages',
'ngResource',
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'angular-google-analytics',
'angular-json-tree',
'angular-loading-bar',
'portainer.templates',
'portainer.filters',
'portainer.rest',
'portainer.helpers',
'portainer.services',
'auth',
'dashboard',
'config',
'configs',
'container',
'containerConsole',
'containerLogs',
'containerStats',
'containerInspect',
'serviceLogs',
'containers',
'createConfig',
'createContainer',
'createNetwork',
'createRegistry',
'createSecret',
'createService',
'createVolume',
'createStack',
'engine',
'endpoint',
'endpointAccess',
'endpoints',
'events',
'image',
'images',
'initAdmin',
'initEndpoint',
'main',
'network',
'networks',
'node',
'registries',
'registry',
'registryAccess',
'secrets',
'secret',
'service',
'services',
'settings',
'settingsAuthentication',
'sidebar',
'stack',
'stacks',
'swarm',
'swarmVisualizer',
'task',
'team',
'teams',
'templates',
'user',
'users',
'userSettings',
'volume',
'volumes',
'portainer.app',
'portainer.docker',
'extension.storidge',
'rzModule']);

View File

@@ -7,7 +7,7 @@ angular.module('portainer')
StateManager.initialize()
.then(function success(state) {
if (state.application.authentication) {
initAuthentication(authManager, Authentication, $rootScope);
initAuthentication(authManager, Authentication, $rootScope, $state);
}
if (state.application.analytics) {
initAnalytics(Analytics, $rootScope);
@@ -30,12 +30,12 @@ angular.module('portainer')
}]);
function initAuthentication(authManager, Authentication, $rootScope) {
function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$on('tokenHasExpired', function() {
$state.go('auth', {error: 'Your session has expired'});
$state.go('portainer.auth', {error: 'Your session has expired'});
});
}

View File

@@ -1,80 +0,0 @@
<rd-header>
<rd-header-title title="Configs list">
<a data-toggle="tooltip" title="Refresh" ui-sref="configs" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Configs</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-file-code-o" title="Configs">
</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-primary" type="button" ui-sref="actions.create.config"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add config</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>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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 ng-click="order('CreatedAt')">
Created at
<span ng-show="sortType == 'CreatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'CreatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr dir-paginate="config in (state.filteredConfigs = ( configs | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="config.Checked" ng-change="selectItem(config)"/></td>
<td><a ui-sref="config({id: config.Id})">{{ config.Name }}</a></td>
<td>{{ config.CreatedAt | getisodate }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="config.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ config.ResourceControl.Ownership ? config.ResourceControl.Ownership : config.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!configs">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="configs.length == 0">
<td colspan="4" class="text-center text-muted">No configs available.</td>
</tr>
</tbody>
</table>
<div ng-if="configs" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@@ -1,60 +0,0 @@
angular.module('configs', [])
.controller('ConfigsController', ['$scope', '$stateParams', '$state', 'ConfigService', 'Notifications', 'Pagination',
function ($scope, $stateParams, $state, ConfigService, Notifications, Pagination) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('configs');
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredConfigs, function (config) {
if (config.Checked !== allSelected) {
config.Checked = allSelected;
$scope.selectItem(config);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
angular.forEach($scope.configs, function (config) {
if (config.Checked) {
ConfigService.remove(config.Id)
.then(function success() {
Notifications.success('Config deleted', config.Id);
var index = $scope.configs.indexOf(config);
$scope.configs.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove config');
});
}
});
};
function initView() {
ConfigService.configs()
.then(function success(data) {
$scope.configs = data;
})
.catch(function error(err) {
$scope.configs = [];
Notifications.error('Failure', err, 'Unable to retrieve configs');
});
}
initView();
}]);

View File

@@ -1,151 +0,0 @@
<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>
</rd-header-title>
<rd-header-content>Containers</rd-header-content>
</rd-header>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Containers">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount || state.noStoppedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount || state.noRunningItemsSelected"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button type="button" class="btn btn-danger 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 || state.noRunningItemsSelected"><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 || state.noPausedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="confirmRemoveAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<a class="btn btn-primary" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a>
</div>
<div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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 ng-click="order('Names')">
Name
<span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
<a data-toggle="tooltip" title="More" ng-click="truncateMore();" ng-show="showMore">
<i class="fa fa-plus-square" aria-hidden="true"></i>
</a>
</th>
<th>
<a ng-click="order('StackName')">
Stack
<span ng-show="sortType == 'StackName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'StackName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="state.displayIP">
<a 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 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 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>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td>
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" 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|truncate: truncate_size}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: truncate_size}}</a></td>
<td>{{ container.StackName ? container.StackName : '-' }}</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
<td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{ PublicURL || 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>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="container.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ container.ResourceControl.Ownership ? container.ResourceControl.Ownership : container.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!containers">
<td colspan="9" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="containers.length == 0">
<td colspan="9" class="text-center text-muted">No containers available.</td>
</tr>
</tbody>
</table>
<div ng-if="containers" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,249 +0,0 @@
angular.module('containers', [])
.controller('ContainersController', ['$q', '$scope', '$state', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', 'LocalStorage',
function ($q, $scope, $state, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider, LocalStorage) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = LocalStorage.getFilterContainerShowAll();
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.state.selectedItemCount = 0;
$scope.truncate_size = 40;
$scope.showMore = true;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.PublicURL = EndpointProvider.endpointPublicURL();
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
$scope.cleanAssociatedVolumes = false;
var update = function (data) {
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status);
EntityListService.rememberPreviousSelection($scope.containers, model, function onSelect(model){
$scope.selectItem(model);
});
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;
});
updateSelectionFlags();
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve containers');
$scope.containers = [];
});
};
var batch = function (items, action, msg) {
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
update({all: $scope.state.displayAll ? 1 : 0});
}
};
angular.forEach(items, function (c) {
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
action({id: c.Id}, {}, function (d) {
Notifications.success('Container ' + msg, c.Id);
complete();
}, function (e) {
Notifications.error('Failure', e, 'Unable to start container');
complete();
});
}
else if (action === Container.remove) {
ContainerService.remove(c, $scope.cleanAssociatedVolumes)
.then(function success() {
var index = items.indexOf(c);
items.splice(index, 1);
Notifications.success('Container successfully removed');
complete();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove container');
complete();
});
}
else if (action === Container.pause) {
action({id: c.Id}, function (d) {
if (d.message) {
Notifications.success('Container is already paused', c.Id);
} else {
Notifications.success('Container ' + msg, c.Id);
}
complete();
}, function (e) {
Notifications.error('Failure', e, 'Unable to pause container');
complete();
});
}
else {
action({id: c.Id}, function (d) {
Notifications.success('Container ' + msg, c.Id);
complete();
}, function (e) {
Notifications.error('Failure', e, 'An error occured');
complete();
});
}
}
});
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredContainers, function (container) {
if (container.Checked !== allSelected) {
container.Checked = allSelected;
toggleItemSelection(container);
}
});
updateSelectionFlags();
};
$scope.selectItem = function (item) {
toggleItemSelection(item);
updateSelectionFlags();
};
$scope.toggleGetAll = function () {
LocalStorage.storeFilterContainerShowAll($scope.state.displayAll);
update({all: $scope.state.displayAll ? 1 : 0});
};
$scope.startAction = function () {
batch($scope.containers, Container.start, 'Started');
};
$scope.stopAction = function () {
batch($scope.containers, Container.stop, 'Stopped');
};
$scope.restartAction = function () {
batch($scope.containers, Container.restart, 'Restarted');
};
$scope.killAction = function () {
batch($scope.containers, Container.kill, 'Killed');
};
$scope.pauseAction = function () {
batch($scope.containers, Container.pause, 'Paused');
};
$scope.unpauseAction = function () {
batch($scope.containers, Container.unpause, 'Unpaused');
};
$scope.removeAction = function () {
batch($scope.containers, Container.remove, 'Removed');
};
$scope.truncateMore = function(size) {
$scope.truncate_size = 80;
$scope.showMore = false;
};
$scope.confirmRemoveAction = function () {
var isOneContainerRunning = false;
angular.forEach($scope.containers, function (c) {
if (c.Checked && c.State === 'running') {
isOneContainerRunning = true;
return;
}
});
var title = 'You are about to remove one or more container.';
if (isOneContainerRunning) {
title = 'You are about to remove one or more running containers.';
}
ModalService.confirmContainerDeletion(
title,
function (result) {
if(!result) { return; }
$scope.cleanAssociatedVolumes = false;
if (result[0]) {
$scope.cleanAssociatedVolumes = true;
}
$scope.removeAction();
}
);
};
function toggleItemSelection(item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
}
function updateSelectionFlags() {
$scope.state.noStoppedItemsSelected = true;
$scope.state.noRunningItemsSelected = true;
$scope.state.noPausedItemsSelected = true;
$scope.containers.forEach(function(container) {
if(!container.Checked) {
return;
}
if(container.Status === 'paused') {
$scope.state.noPausedItemsSelected = false;
} else if(container.Status === 'stopped' ||
container.Status === 'created') {
$scope.state.noStoppedItemsSelected = false;
} else if(container.Status === 'running') {
$scope.state.noRunningItemsSelected = false;
}
} );
}
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;
}
function initView() {
var provider = $scope.applicationState.endpoint.mode.provider;
$q.when(provider !== 'DOCKER_SWARM' || SystemService.info())
.then(function success(data) {
if (provider === 'DOCKER_SWARM') {
$scope.swarm_hosts = retrieveSwarmHostsInfo(data);
}
update({all: $scope.state.displayAll ? 1 : 0});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
});
}
initView();
}]);

View File

@@ -1,28 +0,0 @@
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12 small text-muted">
Secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
</div>
</div>
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Secrets</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSecret()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add a secret
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="secret in formValues.Secrets" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">secret</span>
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
<option value="" selected="selected">Select a secret</option>
</select>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</form>

View File

@@ -1,159 +0,0 @@
<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>
</rd-header-title>
<rd-header-content>Endpoint management</rd-header-content>
</rd-header>
<div class="row" ng-if="!applicationState.application.endpointManagement">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
</rd-widget-header>
<rd-widget-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag.
Endpoint management via the UI is disabled.
<span ng-if="applicationState.application.authentication">You can still manage endpoint access.</span>
</span>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="applicationState.application.endpointManagement">
<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-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-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-3 col-lg-2 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-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 -->
<!-- endpoint-public-url-input -->
<div class="form-group">
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
Public IP
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="endpoint_public_url" ng-model="formValues.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
</div>
</div>
<!-- !endpoint-public-url-input -->
<!-- endpoint-security -->
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
<!-- !endpoint-security -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Name || !formValues.URL || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (formValues.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="addEndpoint()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
</div>
</div>
<!-- !actions -->
</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" ng-if="applicationState.application.endpointManagement">
<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 ng-if="applicationState.application.endpointManagement">
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</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></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td ng-if="applicationState.application.endpointManagement"><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>
<span ng-if="applicationState.application.endpointManagement">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>
<span ng-if="applicationState.application.authentication">
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</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

@@ -1,73 +0,0 @@
<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>
</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

@@ -1,32 +0,0 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Notifications', 'SystemService', 'Pagination',
function ($scope, Notifications, SystemService, 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);
};
function initView() {
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
SystemService.events(from, to)
.then(function success(data) {
$scope.events = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load events');
});
}
initView();
}]);

View File

@@ -1,152 +0,0 @@
<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>
</rd-header-title>
<rd-header-content>Images</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-download" title="Pull image ">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- image-and-registry -->
<div class="form-group">
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
</div>
<!-- !image-and-registry -->
<!-- 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-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Image" ng-click="pullImage()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Pull the image</span>
<span ng-show="state.actionInProgress">Download in progress...</span>
</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">
<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">
<div class="btn-group">
<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>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="!state.selectedItemCount" >
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="confirmRemovalAction(true)" ng-disabled="!state.selectedItemCount" >Force Remove</a></li>
</ul>
</div>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined">
All
</label>
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="'!' + 0">
Used
</label>
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="0">
Unused
</label>
</span>
</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:{ ContainerCount: state.containersCountFilter } | 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 class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::image.ContainerCount === 0">Unused</span>
</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="state.filteredImages.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,100 +0,0 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
function ($scope, $state, ImageService, Notifications, Pagination, ModalService) {
$scope.state = {
pagination_count: Pagination.getPaginationCount('images'),
actionInProgress: false,
selectedItemCount: 0
};
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.formValues = {
Image: '',
Registry: ''
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$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);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.pullImage = function() {
var image = $scope.formValues.Image;
var registry = $scope.formValues.Registry;
$scope.state.actionInProgress = true;
ImageService.pullImage(image, registry, false)
.then(function success(data) {
Notifications.success('Image successfully pulled', image);
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to pull image');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.confirmRemovalAction = function (force) {
ModalService.confirmImageForceRemoval(function (confirmed) {
if(!confirmed) { return; }
$scope.removeAction(force);
});
};
$scope.removeAction = function (force) {
force = !!force;
angular.forEach($scope.images, function (i) {
if (i.Checked) {
ImageService.deleteImage(i.Id, force)
.then(function success(data) {
Notifications.success('Image deleted', i.Id);
var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
});
}
});
};
function fetchImages() {
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
ImageService.images(true)
.then(function success(data) {
$scope.images = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve images');
$scope.images = [];
});
}
fetchImages();
}]);

View File

@@ -1,132 +0,0 @@
<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>
</rd-header-title>
<rd-header-content>Networks</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-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>
<a class="btn btn-primary" type="button" ui-sref="actions.create.network"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add network</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>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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 ng-click="order('StackName')">
Stack
<span ng-show="sortType == 'StackName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'StackName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a 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 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 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 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 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>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && 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.StackName ? network.StackName : '-' }}</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>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="network.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ network.ResourceControl.Ownership ? network.ResourceControl.Ownership : network.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!networks">
<td colspan="9" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="networks.length == 0">
<td colspan="9" 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,67 +0,0 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'NetworkService', 'Notifications', 'Pagination',
function ($scope, $state, Network, NetworkService, Notifications, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function(allSelected) {
angular.forEach($scope.state.filteredNetworks, function (network) {
if (network.Checked !== allSelected) {
network.Checked = allSelected;
$scope.selectItem(network);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
angular.forEach($scope.networks, function (network) {
if (network.Checked) {
Network.remove({id: network.Id}, function (d) {
if (d.message) {
Notifications.error('Error', d, 'Unable to remove network');
} else {
Notifications.success('Network removed', network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
}, function (e) {
Notifications.error('Failure', e, 'Unable to remove network');
});
}
});
};
function initView() {
NetworkService.networks(true, true, true, true)
.then(function success(data) {
$scope.networks = data;
})
.catch(function error(err) {
$scope.networks = [];
Notifications.error('Failure', err, 'Unable to retrieve networks');
});
}
initView();
}]);

View File

@@ -1,151 +0,0 @@
<rd-header>
<rd-header-title title="Registries">
<a data-toggle="tooltip" title="Refresh" ui-sref="registries" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Registry management</rd-header-content>
</rd-header>
<div class="row" ng-if="dockerhub">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title="DockerHub">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- note -->
<div class="form-group">
<span class="col-sm-12 text-muted small">
The DockerHub registry can be used by any user. You can specify the credentials that will be used to push &amp; pull images here.
</span>
</div>
<!-- !note -->
<!-- authentication-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to push/pull private images."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="dockerhub.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="dockerhub.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="hub_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="hub_username" ng-model="dockerhub.Username">
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="hub_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="hub_password" ng-model="dockerhub.Password">
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || dockerhub.Authentication && (!dockerhub.Username || !dockerhub.Password)" ng-click="updateDockerHub()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Update</span>
<span ng-show="state.actionInProgress">Updating DockerHub settings...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title="Available registries">
<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>
<a class="btn btn-primary" type="button" ui-sref="actions.create.registry"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add registry</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>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="registries" 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="registries" 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></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="registry in (state.filteredRegistries = (registries | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="registry.Checked" ng-change="selectItem(registry)" /></td>
<td>
<a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
<span ng-if="registry.Authentication" style="margin-left: 5px;">
<i class="fa fa-shield" aria-hidden="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Authentication is enabled for this registry."></i>
</span>
</td>
<td>{{ registry.URL }}</td>
<td>
<span ng-if="applicationState.application.authentication">
<a ui-sref="registry.access({id: registry.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</span>
</td>
</tr>
<tr ng-if="!registries">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="registries.length == 0">
<td colspan="4" class="text-center text-muted">No registries available.</td>
</tr>
</tbody>
</table>
<div ng-if="registries" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,97 +0,0 @@
angular.module('registries', [])
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'Pagination',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Pagination) {
$scope.state = {
selectedItemCount: 0,
pagination_count: Pagination.getPaginationCount('registries'),
actionInProgress: false
};
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.updateDockerHub = function() {
var dockerhub = $scope.dockerhub;
$scope.state.actionInProgress = true;
DockerHubService.update(dockerhub)
.then(function success(data) {
Notifications.success('DockerHub registry updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update DockerHub details');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$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.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredRegistries, function (registry) {
if (registry.Checked !== allSelected) {
registry.Checked = allSelected;
$scope.selectItem(registry);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function() {
ModalService.confirmDeletion(
'Do you want to remove the selected registries?',
function onConfirm(confirmed) {
if(!confirmed) { return; }
removeRegistries();
}
);
};
function removeRegistries() {
var registries = $scope.registries;
angular.forEach(registries, function (registry) {
if (registry.Checked) {
RegistryService.deleteRegistry(registry.Id)
.then(function success(data) {
var index = registries.indexOf(registry);
registries.splice(index, 1);
Notifications.success('Registry deleted', registry.Name);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove registry');
});
}
});
}
function initView() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub()
})
.then(function success(data) {
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
})
.catch(function error(err) {
$scope.registries = [];
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
initView();
}]);

View File

@@ -1,80 +0,0 @@
<rd-header>
<rd-header-title title="Secrets list">
<a data-toggle="tooltip" title="Refresh" ui-sref="secrets" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Secrets</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-user-secret" title="Secrets">
</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-primary" type="button" ui-sref="actions.create.secret"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add secret</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>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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 ng-click="order('CreatedAt')">
Created at
<span ng-show="sortType == 'CreatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'CreatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
<td>{{ secret.CreatedAt | getisodate }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="secret.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ secret.ResourceControl.Ownership ? secret.ResourceControl.Ownership : secret.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!secrets">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="secrets.length == 0">
<td colspan="4" class="text-center text-muted">No secrets available.</td>
</tr>
</tbody>
</table>
<div ng-if="secrets" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,60 +0,0 @@
angular.module('secrets', [])
.controller('SecretsController', ['$scope', '$state', 'SecretService', 'Notifications', 'Pagination',
function ($scope, $state, SecretService, Notifications, Pagination) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('secrets');
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredSecrets, function (secret) {
if (secret.Checked !== allSelected) {
secret.Checked = allSelected;
$scope.selectItem(secret);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
angular.forEach($scope.secrets, function (secret) {
if (secret.Checked) {
SecretService.remove(secret.Id)
.then(function success() {
Notifications.success('Secret deleted', secret.Id);
var index = $scope.secrets.indexOf(secret);
$scope.secrets.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove secret');
});
}
});
};
function initView() {
SecretService.secrets()
.then(function success(data) {
$scope.secrets = data;
})
.catch(function error(err) {
$scope.secrets = [];
Notifications.error('Failure', err, 'Unable to retrieve secrets');
});
}
initView();
}]);

View File

@@ -1,71 +0,0 @@
<div ng-if="tasks.length > 0 && nodes" id="service-tasks">
<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-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">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ng-click="order('Status.State')">
Status
<span ng-show="sortType == 'Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="service.Mode !== 'global'">
<a 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 ng-click="order('NodeId')">
Node
<span ng-show="sortType == 'NodeId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'NodeId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a 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 | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })" class="monospaced">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
<td ng-if="service.Mode !== 'global'">{{ task.Slot }}</td>
<td>{{ task.NodeId | tasknodename: nodes }}</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>

View File

@@ -1,140 +0,0 @@
<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>
</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-primary" type="button" ui-sref="actions.create.service"><i class="fa fa-plus space-right" aria-hidden="true"></i>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 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 ng-click="order('StackName')">
Stack
<span ng-show="sortType == 'StackName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'StackName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a 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 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>
<th>
<a 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>
<th>
<a ng-click="order('UpdatedAt')">
Updated at
<span ng-show="sortType == 'UpdatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'UpdatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && 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.StackName ? service.StackName : '-' }}</td>
<td>{{ service.Image | hideshasum }}</td>
<td>
{{ service.Mode }}
<code data-toggle="tooltip" title="Replicas">{{ service.Running }}</code>
/
<code data-toggle="tooltip" title="Replicas">{{ service.Replicas }}</code>
<span ng-if="service.Mode === 'replicated' && !service.Scale">
<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>
<td>
<a ng-if="service.Ports && service.Ports.length > 0 && swarmManagerIP" ng-repeat="p in service.Ports" class="image-tag" ng-href="http://{{swarmManagerIP}}:{{p.PublishedPort}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!service.Ports || service.Ports.length === 0 || !swarmManagerIP" >-</span>
</td>
<td>
{{ service.UpdatedAt|getisodate }}
</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="service.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ service.ResourceControl.Ownership ? service.ResourceControl.Ownership : service.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!services">
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="services.length == 0">
<td colspan="7" 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

@@ -1,109 +0,0 @@
angular.module('services', [])
.controller('ServicesController', ['$q', '$scope', '$transition$', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Pagination', 'Task', 'Node', 'NodeHelper', 'ModalService', 'ResourceControlService',
function ($q, $scope, $transition$, $state, Service, ServiceService, ServiceHelper, Notifications, Pagination, Task, Node, NodeHelper, ModalService, ResourceControlService) {
$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) {
var config = ServiceHelper.serviceToConfig(service.Model);
config.Mode.Replicated.Replicas = service.Replicas;
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
Notifications.success('Service successfully scaled', 'New replica count: ' + service.Replicas);
$state.reload();
}, function (e) {
service.Scale = false;
service.Replicas = service.ReplicaCount;
Notifications.error('Failure', e, 'Unable to scale service');
});
};
$scope.removeAction = function() {
ModalService.confirmDeletion(
'Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.',
function onConfirm(confirmed) {
if(!confirmed) { return; }
removeServices();
}
);
};
function removeServices() {
angular.forEach($scope.services, function (service) {
if (service.Checked) {
ServiceService.remove(service)
.then(function success(data) {
Notifications.success('Service successfully deleted');
var index = $scope.services.indexOf(service);
$scope.services.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove service');
});
}
});
}
function mapUsersToServices(users) {
angular.forEach($scope.services, function (service) {
if (service.Metadata) {
var serviceRC = service.Metadata.ResourceControl;
if (serviceRC && serviceRC.OwnerId !== $scope.user.ID) {
angular.forEach(users, function (user) {
if (serviceRC.OwnerId === user.Id) {
service.Owner = user.Username;
}
});
}
}
});
}
function initView() {
$q.all({
services: Service.query({}).$promise,
tasks: Task.query({filters: {'desired-state': ['running','accepted']}}).$promise,
nodes: Node.query({}).$promise
})
.then(function success(data) {
$scope.swarmManagerIP = NodeHelper.getManagerIP(data.nodes);
$scope.services = data.services.map(function (service) {
var runningTasks = data.tasks.filter(function (task) {
return task.ServiceID === service.ID && task.Status.State === 'running';
});
var allTasks = data.tasks.filter(function (task) {
return task.ServiceID === service.ID;
});
var taskNodes = data.nodes.filter(function (node) {
return node.Spec.Availability === 'active' && node.Status.State === 'ready';
});
return new ServiceViewModel(service, runningTasks, allTasks, taskNodes);
});
})
.catch(function error(err) {
$scope.services = [];
Notifications.error('Failure', err, 'Unable to retrieve services');
});
}
initView();
}]);

View File

@@ -1,90 +0,0 @@
<!-- Sidebar -->
<div id="sidebar-wrapper">
<div class="sidebar-header">
<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>
</div>
<div class="sidebar-content">
<ul class="sidebar">
<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>
<div class="sidebar-sublist" ng-if="toggle && displayExternalContributors && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<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.apiVersion >= 1.30 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
</li>
<li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && isAdmin">
<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' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th"></span></a>
</li>
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span>
</li>
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
<a ui-sref="users" ui-sref-active="active">User management <span class="menu-icon fa fa-users"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'users' || $state.current.name === 'user' || $state.current.name === 'teams' || $state.current.name === 'team')">
<a ui-sref="teams" ui-sref-active="active">Teams</a>
</div>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication') && applicationState.application.authentication && isAdmin">
<a ui-sref="settings_authentication" ui-sref-active="active">Authentication</a>
</div>
</li>
</ul>
<div class="sidebar-footer-content">
<img src="images/logo_small.png" class="img-responsive logo" alt="Portainer">
<span class="version">{{ uiVersion }}</span>
</div>
</div>
</div>
<!-- End Sidebar -->

View File

@@ -1,119 +0,0 @@
<rd-header>
<rd-header-title title="Stacks list">
<a data-toggle="tooltip" title="Refresh" ui-sref="stacks" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Stacks</rd-header-content>
</rd-header>
<div class="row" ng-if="state.DisplayInformationPanel">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="Information"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<span class="col-sm-12 text-muted small">
Stacks marked with the <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true"></i> icon are external stacks that were created outside of Portainer. You'll not be able to execute any actions against these stacks.
</span>
</div>
<div class="col-sm-12 form-section-title">
Filters
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Display external stacks
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.DisplayExternalStacks"><i></i>
</label>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-th-list" title="Stacks">
<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-primary" type="button" ui-sref="actions.create.stack"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack</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>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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 ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr dir-paginate="stack in (state.filteredStacks = ( stacks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-if="state.DisplayExternalStacks || (!state.DisplayExternalStacks && !stack.External)">
<td><input type="checkbox" ng-model="stack.Checked" ng-change="selectItem(stack)" ng-disabled="!stack.Id"/></td>
<td>
<span ng-if="stack.Id">
<a ui-sref="stack({ id: stack.Id })">{{ stack.Name }}</a>
</span>
<span ng-if="!stack.Id">
{{ stack.Name }} <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true"></i>
</span>
</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="stack.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ stack.ResourceControl.Ownership ? stack.ResourceControl.Ownership : stack.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!stacks">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="stacks.length === 0">
<td colspan="3" class="text-center text-muted">No stacks available.</td>
</tr>
</tbody>
</table>
<div ng-if="stacks" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@@ -1,84 +0,0 @@
angular.module('stacks', [])
.controller('StacksController', ['$scope', 'Notifications', 'Pagination', 'StackService', 'ModalService',
function ($scope, Notifications, Pagination, StackService, ModalService) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('stacks');
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.state.DisplayInformationPanel = false;
$scope.state.DisplayExternalStacks = true;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stacks', $scope.state.pagination_count);
};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredStacks, function (stack) {
if (stack.Id && stack.Checked !== allSelected) {
stack.Checked = allSelected;
$scope.selectItem(stack);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
ModalService.confirmDeletion(
'Do you want to remove the selected stack(s)? Associated services will be removed as well.',
function onConfirm(confirmed) {
if(!confirmed) { return; }
deleteSelectedStacks();
}
);
};
function deleteSelectedStacks() {
angular.forEach($scope.stacks, function (stack) {
if (stack.Checked) {
StackService.remove(stack)
.then(function success() {
Notifications.success('Stack deleted', stack.Name);
var index = $scope.stacks.indexOf(stack);
$scope.stacks.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name);
});
}
});
}
function initView() {
StackService.stacks(true)
.then(function success(data) {
var stacks = data;
for (var i = 0; i < stacks.length; i++) {
var stack = stacks[i];
if (stack.External) {
$scope.state.DisplayInformationPanel = true;
break;
}
}
$scope.stacks = stacks;
})
.catch(function error(err) {
$scope.stacks = [];
Notifications.error('Failure', err, 'Unable to retrieve stacks');
});
}
initView();
}]);

View File

@@ -1,244 +0,0 @@
<rd-header>
<rd-header-title title="Cluster overview">
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Swarm</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td>
</tr>
<tr>
<td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Strategy</td>
<td>{{ swarm.Strategy }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="swarm.visualizer"><i class="fa fa-object-group space-right" aria-hidden="true"></i>Go to cluster visualizer</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-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('Hostname')">
Name
<span ng-show="sortType == 'Hostname' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Hostname' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Role')">
Role
<span ng-show="sortType == 'Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('CPUs')">
CPU
<span ng-show="sortType == 'CPUs' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'CPUs' && 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('EngineVersion')">
Engine
<span ng-show="sortType == 'EngineVersion' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'EngineVersion' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.endpoint.apiVersion >= 1.25">
<a ui-sref="swarm" ng-click="order('Addr')">
IP Address
<span ng-show="sortType == 'Addr' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Addr' && 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 = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td>
<a ui-sref="node({id: node.Id})" ng-if="!applicationState.application.authentication || isAdmin">{{ node.Hostname }}</a>
<span ng-if="applicationState.application.authentication && !isAdmin">{{ node.Hostname }}</span>
</td>
<td>{{ node.Role }}</td>
<td>{{ node.CPUs / 1000000000 }}</td>
<td>{{ node.Memory|humansize }}</td>
<td>{{ node.EngineVersion }}</td>
<td ng-if="applicationState.endpoint.apiVersion >= 1.25">{{ node.Addr }}</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>

View File

@@ -1,120 +0,0 @@
<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>
</rd-header-title>
<rd-header-content>Volumes</rd-header-content>
</rd-header>
<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">
<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-primary" type="button" ui-sref="actions.create.volume"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="undefined">
All
</label>
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="false">
Attached
</label>
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="true">
Unused
</label>
</span>
</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="volumes" ng-click="order('Id')">
Name
<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="volumes" ng-click="order('StackName')">
Stack
<span ng-show="sortType == 'StackName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'StackName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="volumes" 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="volumes" ng-click="order('Mountpoint')">
Mount point
<span ng-show="sortType == 'Mountpoint' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ui-sref="volumes" ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:{dangling: state.danglingVolumesOnly} | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>
<a ui-sref="volume({id: volume.Id})" class="monospaced">{{ volume.Id|truncate:25 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="volume.dangling">Unused</span></td>
</td>
<td>{{ volume.StackName ? volume.StackName : '-' }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint | truncatelr }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="volume.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ volume.ResourceControl.Ownership ? volume.ResourceControl.Ownership : volume.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!volumes">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="volumes.length === 0 || state.filteredVolumes.length === 0">
<td colspan="5" 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>

View File

@@ -1,79 +0,0 @@
angular.module('volumes', [])
.controller('VolumesController', ['$q', '$scope', 'VolumeService', 'Notifications', 'Pagination',
function ($q, $scope, VolumeService, Notifications, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('volumes');
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Id';
$scope.sortReverse = false;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('volumes', $scope.state.pagination_count);
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredVolumes, function (volume) {
if (volume.Checked !== allSelected) {
volume.Checked = allSelected;
$scope.selectItem(volume);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
angular.forEach($scope.volumes, function (volume) {
if (volume.Checked) {
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume deleted', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
}
});
};
function initView() {
$q.all({
attached: VolumeService.volumes({
filters: {
'dangling': ['false']
}
}),
dangling: VolumeService.volumes({
filters: {
'dangling': ['true']
}
})
})
.then(function success(data) {
$scope.volumes = data.attached.map(function(volume) {
volume.dangling = false;
return volume;
}).concat(data.dangling.map(function(volume) {
volume.dangling = true;
return volume;
}));
}).catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve volumes');
});
}
initView();
}]);

View File

@@ -1,6 +1,6 @@
angular.module('portainer')
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider',
function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
.config(['$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider',
function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
'use strict';
var environment = '@@ENVIRONMENT';
@@ -16,7 +16,7 @@ angular.module('portainer')
return LocalStorage.getJWT();
}],
unauthenticatedRedirector: ['$state', function($state) {
$state.go('auth', {error: 'Your session has expired'});
$state.go('portainer.auth', {error: 'Your session has expired'});
}]
});
$httpProvider.interceptors.push('jwtInterceptor');
@@ -37,5 +37,4 @@ angular.module('portainer')
cfpLoadingBarProvider.parentSelector = '#loadingbar-placeholder';
$urlRouterProvider.otherwise('/auth');
configureRoutes($stateProvider);
}]);

View File

@@ -11,4 +11,5 @@ angular.module('portainer')
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10);
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600);

View File

@@ -1,8 +0,0 @@
angular.module('portainer').component('porAccessManagement', {
templateUrl: 'app/directives/accessManagement/porAccessManagement.html',
controller: 'porAccessManagementController',
bindings: {
accessControlledEntity: '<',
updateAccess: '&'
}
});

View File

@@ -1,17 +0,0 @@
angular
.module('portainer')
.directive('rdHeaderTitle', ['Authentication', function rdHeaderTitle(Authentication) {
var directive = {
requires: '^rdHeader',
scope: {
title: '@'
},
link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getUserDetails().username;
},
transclude: true,
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',
restrict: 'E'
};
return directive;
}]);

View File

@@ -1,8 +0,0 @@
angular.module('portainer').component('porImageRegistry', {
templateUrl: 'app/directives/imageRegistry/porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
'image': '=',
'registry': '='
}
});

View File

@@ -1,27 +0,0 @@
angular.module('portainer')
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'Notifications',
function ($q, RegistryService, DockerHubService, Notifications) {
var ctrl = this;
function initComponent() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub()
})
.then(function success(data) {
var dockerhub = data.dockerhub;
var registries = data.registries;
ctrl.availableRegistries = [dockerhub].concat(registries);
if (!ctrl.registry.Id) {
ctrl.registry = dockerhub;
} else {
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
initComponent();
}]);

View File

@@ -1,8 +0,0 @@
angular.module('portainer').component('porServiceList', {
templateUrl: 'app/directives/serviceList/porServiceList.html',
controller: 'porServiceListController',
bindings: {
'services': '<',
'nodes': '<'
}
});

View File

@@ -1,98 +0,0 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Associated services">
<div class="pull-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count" ng-change="$ctrl.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-sm-12">
<div class="pull-right">
<input type="text" id="filter" ng-model="$ctrl.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 ng-click="$ctrl.order('Name')">
Name
<span ng-show="$ctrl.sortType === 'Name' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Name' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('Image')">
Image
<span ng-show="$ctrl.sortType === 'Image' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Image' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('Mode')">
Scheduling mode
<span ng-show="$ctrl.sortType === 'Mode' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Mode' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('Ports')">
Published Ports
<span ng-show="$ctrl.sortType === 'Ports' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Ports' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('UpdatedAt')">
Updated at
<span ng-show="$ctrl.sortType === 'UpdatedAt' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'UpdatedAt' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="service in $ctrl.services | filter:$ctrl.state.filter | orderBy:$ctrl.sortType:$ctrl.sortReverse | itemsPerPage:$ctrl.state.pagination_count" pagination-id="services_list">
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
<td>{{ service.Image | hideshasum }}</td>
<td>
{{ service.Mode }}
<code data-toggle="tooltip" title="Replicas">{{ service.Tasks | runningtaskscount }}</code>
/
<code data-toggle="tooltip" title="Replicas">{{ service.Mode === 'replicated' ? service.Replicas : ($ctrl.nodes | availablenodecount) }}</code>
</td>
<td>
<a ng-if="service.Ports && service.Ports.length > 0" ng-repeat="p in service.Ports" class="image-tag" ng-href="http://{{$ctrl.state.publicURL}}:{{p.PublishedPort}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!service.Ports || service.Ports.length === 0" >-</span>
</td>
<td>
{{ service.UpdatedAt|getisodate }}
</td>
</tr>
<tr ng-if="!$ctrl.services">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="($ctrl.services | filter:$ctrl.state.filter | orderBy:$ctrl.sortType:$ctrl.sortReverse | itemsPerPage: $ctrl.state.pagination_count).length === 0">
<td colspan="5" class="text-center text-muted">No services available.</td>
</tr>
</tbody>
</table>
<div ng-if="$ctrl.services" class="pull-left pagination-controls">
<dir-pagination-controls pagination-id="services_list"></dir-pagination-controls >
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@@ -1,21 +0,0 @@
angular.module('portainer')
.controller('porServiceListController', ['EndpointProvider', 'Pagination',
function (EndpointProvider, Pagination) {
var ctrl = this;
ctrl.state = {
pagination_count: Pagination.getPaginationCount('services_list'),
publicURL: EndpointProvider.endpointPublicURL()
};
ctrl.sortType = 'Name';
ctrl.sortReverse = false;
ctrl.order = function(sortType) {
ctrl.sortReverse = (ctrl.sortType === sortType) ? !ctrl.sortReverse : false;
ctrl.sortType = sortType;
};
ctrl.changePaginationCount = function() {
Pagination.setPaginationCount('services_list', ctrl.state.pagination_count);
};
}]);

View File

@@ -1,12 +0,0 @@
angular.module('portainer').component('porSlider', {
templateUrl: 'app/directives/slider/porSlider.html',
controller: 'porSliderController',
bindings: {
model: '=',
onChange: '&',
floor: '<',
ceil: '<',
step: '<',
precision: '<'
}
});

View File

@@ -1,8 +0,0 @@
angular.module('portainer').component('porTaskList', {
templateUrl: 'app/directives/taskList/porTaskList.html',
controller: 'porTaskListController',
bindings: {
'tasks': '<',
'nodes': '<'
}
});

View File

@@ -1,78 +0,0 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count" ng-change="$ctrl.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-sm-12">
<div class="pull-right">
<input type="text" id="filter" ng-model="$ctrl.state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ng-click="$ctrl.order('Status.State')">
Status
<span ng-show="$ctrl.sortType === 'Status.State' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Status.State' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="service.Mode !== 'global'">
<a ng-click="$ctrl.order('Slot')">
Slot
<span ng-show="$ctrl.sortType === 'Slot' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Slot' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('NodeId')">
Node
<span ng-show="$ctrl.sortType === 'NodeId' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'NodeId' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.order('Updated')">
Last update
<span ng-show="$ctrl.sortType === 'Updated' && !$ctrl.sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.sortType === 'Updated' && $ctrl.sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in $ctrl.tasks | filter:$ctrl.state.filter | orderBy:$ctrl.sortType:$ctrl.sortReverse | itemsPerPage:$ctrl.state.pagination_count">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status.State | taskstatusbadge }}">{{ task.Status.State }}</span></td>
<td>{{ task.Slot ? task.Slot : '-' }}</td>
<td>{{ task.NodeId | tasknodename: $ctrl.nodes }}</td>
<td>{{ task.Updated | getisodate }}</td>
</tr>
<tr ng-if="!$ctrl.tasks">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="($ctrl.tasks | filter:$ctrl.state.filter | orderBy:$ctrl.sortType:$ctrl.sortReverse | itemsPerPage: $ctrl.state.pagination_count).length === 0">
<td colspan="5" class="text-center text-muted">No tasks available.</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,19 +0,0 @@
angular.module('portainer')
.controller('porTaskListController', ['Pagination',
function (Pagination) {
var ctrl = this;
ctrl.state = {
pagination_count: Pagination.getPaginationCount('tasks_list')
};
ctrl.sortType = 'Updated';
ctrl.sortReverse = true;
ctrl.order = function(sortType) {
ctrl.sortReverse = (ctrl.sortType === sortType) ? !ctrl.sortReverse : false;
ctrl.sortType = sortType;
};
ctrl.changePaginationCount = function() {
Pagination.setPaginationCount('tasks_list', ctrl.state.pagination_count);
};
}]);

484
app/docker/__module.js Normal file
View File

@@ -0,0 +1,484 @@
angular.module('portainer.docker', ['portainer.app'])
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
var docker = {
name: 'docker',
parent: 'root',
abstract: true
};
var configs = {
name: 'docker.configs',
url: '/configs',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/configs.html',
controller: 'ConfigsController'
}
}
};
var config = {
name: 'docker.configs.config',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/edit/config.html',
controller: 'ConfigController'
}
}
};
var configCreation = {
name: 'docker.configs.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/create/createconfig.html',
controller: 'CreateConfigController'
}
}
};
var containers = {
name: 'docker.containers',
url: '/containers',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/containers.html',
controller: 'ContainersController'
}
},
params: {
selectedContainers: []
}
};
var container = {
name: 'docker.containers.container',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/edit/container.html',
controller: 'ContainerController'
}
}
};
var containerConsole = {
name: 'docker.containers.container.console',
url: '/console',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/console/containerconsole.html',
controller: 'ContainerConsoleController'
}
}
};
var containerCreation = {
name: 'docker.containers.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/create/createcontainer.html',
controller: 'CreateContainerController'
}
},
params: {
from: ''
}
};
var containerInspect = {
name: 'docker.containers.container.inspect',
url: '/inspect',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/inspect/containerinspect.html',
controller: 'ContainerInspectController'
}
}
};
var containerLogs = {
name: 'docker.containers.container.logs',
url: '/logs',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/logs/containerlogs.html',
controller: 'ContainerLogsController'
}
}
};
var containerStats = {
name: 'docker.containers.container.stats',
url: '/stats',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/stats/containerstats.html',
controller: 'ContainerStatsController'
}
}
};
var dashboard = {
name: 'docker.dashboard',
url: '/dashboard',
views: {
'content@': {
templateUrl: 'app/docker/views/dashboard/dashboard.html',
controller: 'DashboardController'
}
}
};
var engine = {
name: 'docker.engine',
url: '/engine',
views: {
'content@': {
templateUrl: 'app/docker/views/engine/engine.html',
controller: 'EngineController'
}
}
};
var events = {
name: 'docker.events',
url: '/events',
views: {
'content@': {
templateUrl: 'app/docker/views/events/events.html',
controller: 'EventsController'
}
}
};
var images = {
name: 'docker.images',
url: '/images',
views: {
'content@': {
templateUrl: 'app/docker/views/images/images.html',
controller: 'ImagesController'
}
}
};
var image = {
name: 'docker.images.image',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/images/edit/image.html',
controller: 'ImageController'
}
}
};
var networks = {
name: 'docker.networks',
url: '/networks',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/networks.html',
controller: 'NetworksController'
}
}
};
var network = {
name: 'docker.networks.network',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/edit/network.html',
controller: 'NetworkController'
}
}
};
var networkCreation = {
name: 'docker.networks.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/create/createnetwork.html',
controller: 'CreateNetworkController'
}
}
};
var nodes = {
name: 'docker.nodes',
url: '/nodes',
abstract: true
};
var node = {
name: 'docker.nodes.node',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/nodes/edit/node.html',
controller: 'NodeController'
}
}
};
var secrets = {
name: 'docker.secrets',
url: '/secrets',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/secrets.html',
controller: 'SecretsController'
}
}
};
var secret = {
name: 'docker.secrets.secret',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/edit/secret.html',
controller: 'SecretController'
}
}
};
var secretCreation = {
name: 'docker.secrets.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/create/createsecret.html',
controller: 'CreateSecretController'
}
}
};
var services = {
name: 'docker.services',
url: '/services',
views: {
'content@': {
templateUrl: 'app/docker/views/services/services.html',
controller: 'ServicesController'
}
}
};
var service = {
name: 'docker.services.service',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/services/edit/service.html',
controller: 'ServiceController'
}
}
};
var serviceCreation = {
name: 'docker.services.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/services/create/createservice.html',
controller: 'CreateServiceController'
}
}
};
var serviceLogs = {
name: 'docker.services.service.logs',
url: '/logs',
views: {
'content@': {
templateUrl: 'app/docker/views/services/logs/servicelogs.html',
controller: 'ServiceLogsController'
}
}
};
var stacks = {
name: 'docker.stacks',
url: '/stacks',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/stacks.html',
controller: 'StacksController'
}
}
};
var stack = {
name: 'docker.stacks.stack',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/edit/stack.html',
controller: 'StackController'
}
}
};
var stackCreation = {
name: 'docker.stacks.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/create/createstack.html',
controller: 'CreateStackController'
}
}
};
var swarm = {
name: 'docker.swarm',
url: '/swarm',
views: {
'content@': {
templateUrl: 'app/docker/views/swarm/swarm.html',
controller: 'SwarmController'
}
}
};
var swarmVisualizer = {
name: 'docker.swarm.visualizer',
url: '/visualizer',
views: {
'content@': {
templateUrl: 'app/docker/views/swarm/visualizer/swarmvisualizer.html',
controller: 'SwarmVisualizerController'
}
}
};
var tasks = {
name: 'docker.tasks',
url: '/tasks',
abstract: true
};
var task = {
name: 'docker.tasks.task',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/tasks/edit/task.html',
controller: 'TaskController'
}
}
};
var templates = {
name: 'docker.templates',
url: '/templates',
views: {
'content@': {
templateUrl: 'app/docker/views/templates/templates.html',
controller: 'TemplatesController'
}
},
params: {
key: 'containers',
hide_descriptions: false
}
};
var templatesLinuxServer = {
name: 'docker.templates.linuxserver',
url: '/linuxserver',
views: {
'content@': {
templateUrl: 'app/docker/views/templates/templates.html',
controller: 'TemplatesController'
}
},
params: {
key: 'linuxserver.io',
hide_descriptions: true
}
};
var volumes = {
name: 'docker.volumes',
url: '/volumes',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/volumes.html',
controller: 'VolumesController'
}
}
};
var volume = {
name: 'docker.volumes.volume',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/edit/volume.html',
controller: 'VolumeController'
}
}
};
var volumeCreation = {
name: 'docker.volumes.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/create/createvolume.html',
controller: 'CreateVolumeController'
}
}
};
$stateRegistryProvider.register(configs);
$stateRegistryProvider.register(config);
$stateRegistryProvider.register(configCreation);
$stateRegistryProvider.register(containers);
$stateRegistryProvider.register(container);
$stateRegistryProvider.register(containerConsole);
$stateRegistryProvider.register(containerCreation);
$stateRegistryProvider.register(containerInspect);
$stateRegistryProvider.register(containerLogs);
$stateRegistryProvider.register(containerStats);
$stateRegistryProvider.register(docker);
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(engine);
$stateRegistryProvider.register(events);
$stateRegistryProvider.register(images);
$stateRegistryProvider.register(image);
$stateRegistryProvider.register(networks);
$stateRegistryProvider.register(network);
$stateRegistryProvider.register(networkCreation);
$stateRegistryProvider.register(nodes);
$stateRegistryProvider.register(node);
$stateRegistryProvider.register(secrets);
$stateRegistryProvider.register(secret);
$stateRegistryProvider.register(secretCreation);
$stateRegistryProvider.register(services);
$stateRegistryProvider.register(service);
$stateRegistryProvider.register(serviceCreation);
$stateRegistryProvider.register(serviceLogs);
$stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackCreation);
$stateRegistryProvider.register(swarm);
$stateRegistryProvider.register(swarmVisualizer);
$stateRegistryProvider.register(tasks);
$stateRegistryProvider.register(task);
$stateRegistryProvider.register(templates);
$stateRegistryProvider.register(templatesLinuxServer);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeCreation);
}]);

View File

@@ -0,0 +1,108 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.configs.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add config
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreatedAt')">
Creation Date
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.configs.config({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.CreatedAt | getisodate }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No config available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('configsDatatable', {
templateUrl: 'app/docker/components/datatables/configs-datatable/configsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<'
}
});

View File

@@ -0,0 +1,82 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
</div>
<div class="actionBar">
<form class="form-horizontal">
<div class="row">
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a network</label>
<div class="col-sm-5 col-lg-4">
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in $ctrl.availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-1">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="$ctrl.joinNetworkActionInProgress || !$ctrl.selectedNetwork" ng-click="$ctrl.joinNetworkAction($ctrl.container, $ctrl.selectedNetwork)" button-spinner="$ctrl.joinNetworkActionInProgress">
<span ng-hide="$ctrl.joinNetworkActionInProgress">Join network</span>
<span ng-show="$ctrl.joinNetworkActionInProgress">Joining network...</span>
</button>
</div>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Network</th>
<th>IP Address</th>
<th>Gateway</th>
<th>MAC Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.networks.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-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, value.NetworkID)">
<span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash space-right" aria-hidden="true"></i> Leave network</span>
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
</button>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.dataset.length === 0">
<td colspan="5" class="text-center text-muted">No network available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,16 @@
angular.module('portainer.docker').component('containerNetworksDatatable', {
templateUrl: 'app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
container: '<',
availableNetworks: '<',
joinNetworkAction: '<',
joinNetworkActionInProgress: '<',
leaveNetworkActionInProgress: '<',
leaveNetworkAction: '<'
}
});

View File

@@ -0,0 +1,61 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th ng-repeat="header in $ctrl.headerset">
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredProcesses = ($ctrl.dataset | filter:$ctrl.state.textFilter | itemsPerPage: $ctrl.state.paginatedItemLimit))">
<td ng-repeat="info in item track by $index">{{ info }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="{{ $ctrl.headerset.length }}" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredProcesses.length === 0">
<td colspan="{{ $ctrl.headerset.length }}" class="text-center text-muted">No process available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,14 @@
angular.module('portainer.docker').component('containerProcessesDatatable', {
templateUrl: 'app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '=',
headerset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<'
}
});

View File

@@ -0,0 +1,254 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div class="md-checkbox">
<input id="setting_container_trunc" type="checkbox" ng-model="$ctrl.settings.truncateContainerName" ng-change="$ctrl.onSettingsContainerNameTruncateChange()"/>
<label for="setting_container_trunc">Truncate container name</label>
</div>
</div>
<div class="menuHeader">
Quick actions
</div>
<div class="menuContent">
<div class="md-checkbox">
<input id="setting_show_stats" type="checkbox" ng-model="$ctrl.settings.showQuickActionStats" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_stats">Stats</label>
</div>
<div class="md-checkbox">
<input id="setting_show_logs" type="checkbox" ng-model="$ctrl.settings.showQuickActionLogs" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_logs">Logs</label>
</div>
<div class="md-checkbox">
<input id="setting_show_console" type="checkbox" ng-model="$ctrl.settings.showQuickActionConsole" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_console">Console</label>
</div>
<div class="md-checkbox">
<input id="setting_show_inspect" type="checkbox" ng-model="$ctrl.settings.showQuickActionInspect" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_inspect">Inspect</label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="actionBar">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-sm btn-success" ng-click="$ctrl.startAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.state.noStoppedItemsSelected">
<i class="fa fa-play space-right" aria-hidden="true"></i>Start
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.stopAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.state.noRunningItemsSelected">
<i class="fa fa-stop space-right" aria-hidden="true"></i>Stop
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.killAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0">
<i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill
</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.restartAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0">
<i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart
</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.pauseAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.state.noRunningItemsSelected">
<i class="fa fa-pause space-right" aria-hidden="true"></i>Pause
</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.resumeAction($ctrl.state.selectedItems)"
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.state.noPausedItemsSelected">
<i class="fa fa-play space-right" aria-hidden="true"></i>Resume
</button>
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.containers.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Names')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Names' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Names' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
<a ng-click="$ctrl.changeOrderBy('Status')">
State
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
<div>
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
</div>
<div class="dropdown-menu" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Filter by state
</div>
<div class="menuContent">
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
<input id="filter_state_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()"/>
<label for="filter_state_{{ $index }}">{{ filter.label }}</label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
</div>
</div>
</div>
</th>
<th ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
Quick actions
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IP')">
IP Address
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.swarmContainers">
<a ng-click="$ctrl.changeOrderBy('Host')">
Host IP
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Host' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Host' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Ports')">
Published Ports
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.containers.container({ id: item.Id })" ng-if="!$ctrl.swarmContainers">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
<a ui-sref="docker.containers.container({ id: item.Id })" ng-if="$ctrl.swarmContainers">{{ item | swarmcontainername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
</td>
<td>
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
</td>
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
</div>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td><a ui-sref="docker.images.image({ id: item.Image })">{{ item.Image | trimshasum }}</a></td>
<td>{{ item.IP ? item.IP : '-' }}</td>
<td ng-if="$ctrl.swarmContainers">{{ item.hostIP }}</td>
<td>
<a ng-if="item.Ports.length > 0" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl || p.host }}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.public }}:{{ p.private }}
</a>
<span ng-if="item.Ports.length == 0" >-</span>
</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="8" class="text-center text-muted">No container available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,24 @@
angular.module('portainer.docker').component('containersDatatable', {
templateUrl: 'app/docker/components/datatables/containers-datatable/containersDatatable.html',
controller: 'ContainersDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
swarmContainers: '<',
publicUrl: '<',
containerNameTruncateSize: '<',
startAction: '<',
stopAction: '<',
killAction: '<',
restartAction: '<',
pauseAction: '<',
resumeAction: '<',
removeAction: '<'
}
});

View File

@@ -0,0 +1,190 @@
angular.module('portainer.docker')
.controller('ContainersDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
var ctrl = this;
this.state = {
selectAll: false,
orderBy: this.orderBy,
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
displayTextFilter: false,
selectedItemCount: 0,
selectedItems: []
};
this.settings = {
open: false,
truncateContainerName: true,
containerNameTruncateSize: 32,
showQuickActionStats: true,
showQuickActionLogs: true,
showQuickActionConsole: true,
showQuickActionInspect: true
};
this.filters = {
state: {
open: false,
enabled: false,
values: []
}
};
this.changeOrderBy = function(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
};
this.toggleItemSelection = function(item) {
if (item.Checked) {
this.state.selectedItemCount++;
this.state.selectedItems.push(item);
} else {
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
this.state.selectedItemCount--;
}
};
this.selectItem = function(item) {
this.toggleItemSelection(item);
this.updateSelectionState();
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.toggleItemSelection(item);
}
}
this.updateSelectionState();
};
this.updateSelectionState = function() {
this.state.noStoppedItemsSelected = true;
this.state.noRunningItemsSelected = true;
this.state.noPausedItemsSelected = true;
for (var i = 0; i < this.dataset.length; i++) {
var item = this.dataset[i];
if (item.Checked) {
this.updateSelectionStateBasedOnItemStatus(item);
}
}
};
this.updateSelectionStateBasedOnItemStatus = function(item) {
if (item.Status === 'paused') {
this.state.noPausedItemsSelected = false;
} else if (['stopped', 'created'].indexOf(item.Status) !== -1) {
this.state.noStoppedItemsSelected = false;
} else if (['running', 'healthy', 'unhealthy', 'starting'].indexOf(item.Status) !== -1) {
this.state.noRunningItemsSelected = false;
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
this.updateDisplayTextFilter = function() {
this.state.displayTextFilter = !this.state.displayTextFilter;
if (!this.state.displayTextFilter) {
delete this.state.textFilter;
}
};
this.applyFilters = function(value, index, array) {
var container = value;
var filters = ctrl.filters;
for (var i = 0; i < filters.state.values.length; i++) {
var filter = filters.state.values[i];
if (container.Status === filter.label && filter.display) {
return true;
}
}
return false;
};
this.onStateFilterChange = function() {
var filters = this.filters.state.values;
var filtered = false;
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
if (!filter.display) {
filtered = true;
}
}
this.filters.state.enabled = filtered;
DatatableService.setDataTableFilters(this.tableKey, this.filters);
};
this.onSettingsContainerNameTruncateChange = function() {
if (this.settings.truncateContainerName) {
this.settings.containerNameTruncateSize = 32;
} else {
this.settings.containerNameTruncateSize = 256;
}
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.onSettingsQuickActionChange = function() {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.prepareTableFromDataset = function() {
var availableStateFilters = [];
for (var i = 0; i < this.dataset.length; i++) {
var item = this.dataset[i];
if (item.Checked) {
this.selectItem(item);
}
availableStateFilters.push({ label: item.Status, display: true });
}
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
};
this.updateStoredFilters = function(storedFilters) {
var datasetFilters = this.filters.state.values;
for (var i = 0; i < datasetFilters.length; i++) {
var filter = datasetFilters[i];
existingFilter = _.find(storedFilters, ['label', filter.label]);
if (existingFilter && !existingFilter.display) {
filter.display = existingFilter.display;
this.filters.state.enabled = true;
}
}
};
this.$onInit = function() {
setDefaults(this);
this.prepareTableFromDataset();
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.updateStoredFilters(storedFilters.state.values);
}
this.filters.state.open = false;
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
}
this.settings.open = false;
};
function setDefaults(ctrl) {
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
}
}]);

View File

@@ -0,0 +1,81 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Time')">
Date
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Type')">
Category
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Details')">
Details
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Details' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Details' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>{{ item.Time | getisodatefromtimestamp }}</td>
<td>{{ item.Type }}</td>
<td>{{ item.Details }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No event available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,13 @@
angular.module('portainer.docker').component('eventsDatatable', {
templateUrl: 'app/docker/components/datatables/events-datatable/eventsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<'
}
});

View File

@@ -0,0 +1,144 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems, false)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="$ctrl.state.selectedItemCount === 0">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
</ul>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
<thead>
<tr>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.usage.open">
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Id')">
Id
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
<div>
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.usage.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.usage.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
</div>
<div class="dropdown-menu" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Filter by usage
</div>
<div class="menuContent">
<div class="md-checkbox">
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUsedImages" ng-change="$ctrl.onUsageFilterChange()"/>
<label for="filter_usage_usedImages">Used images</label>
</div>
<div class="md-checkbox">
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUnusedImages" ng-change="$ctrl.onUsageFilterChange()"/>
<label for="filter_usage_unusedImages">Unused images</label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.usage.open = false;">Close</a>
</div>
</div>
</div>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RepoTags')">
Tags
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('VirtualSize')">
Size
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Created')">
Created
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.images.image({id: item.Id})" class="monospaced">{{ item.Id | truncate:20 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::item.ContainerCount === 0">Unused</span>
</td>
<td>
<span class="label label-primary image-tag" ng-repeat="tag in (item | repotags)">{{ tag }}</span>
</td>
<td>{{ item.VirtualSize | humansize }}</td>
<td>{{ item.Created | getisodatefromtimestamp }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No image available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('imagesDatatable', {
templateUrl: 'app/docker/components/datatables/images-datatable/imagesDatatable.html',
controller: 'ImagesDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
removeAction: '<',
forceRemoveAction: '<'
}
});

View File

@@ -0,0 +1,102 @@
angular.module('portainer.docker')
.controller('ImagesDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
var ctrl = this;
this.state = {
selectAll: false,
orderBy: this.orderBy,
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
displayTextFilter: false,
selectedItemCount: 0,
selectedItems: []
};
this.filters = {
usage: {
open: false,
enabled: false,
showUsedImages: true,
showUnusedImages: true
}
};
this.changeOrderBy = function(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
};
this.selectItem = function(item) {
if (item.Checked) {
this.state.selectedItemCount++;
this.state.selectedItems.push(item);
} else {
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
this.state.selectedItemCount--;
}
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.selectItem(item);
}
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
this.updateDisplayTextFilter = function() {
this.state.displayTextFilter = !this.state.displayTextFilter;
if (!this.state.displayTextFilter) {
delete this.state.textFilter;
}
};
this.applyFilters = function(value, index, array) {
var image = value;
var filters = ctrl.filters;
if ((image.ContainerCount === 0 && filters.usage.showUnusedImages)
|| (image.ContainerCount !== 0 && filters.usage.showUsedImages)) {
return true;
}
return false;
};
this.onUsageFilterChange = function() {
var filters = this.filters.usage;
var filtered = false;
if (!filters.showUsedImages || !filters.showUnusedImages) {
filtered = true;
}
this.filters.usage.enabled = filtered;
DatatableService.setDataTableFilters(this.tableKey, this.filters);
};
this.$onInit = function() {
setDefaults(this);
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
this.filters.usage.open = false;
};
function setDefaults(ctrl) {
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
}
}]);

View File

@@ -0,0 +1,148 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.networks.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add network
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Scope')">
Scope
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Scope' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Scope' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Driver')">
Driver
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Driver' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Driver' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IPAM.Driver')">
IPAM Driver
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Driver' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Driver' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IPAM.Config[0].Subnet')">
IPAM Subnet
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Config[0].Subnet' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Config[0].Subnet' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IPAM.Config[0].Gateway')">
IPAM Gateway
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Config[0].Gateway' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAM.Config[0].Gateway' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.networks.network({id: item.Id})" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Scope }}</td>
<td>{{ item.Driver }}</td>
<td>{{ item.IPAM.Driver }}</td>
<td>{{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="8" class="text-center text-muted">No network available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('networksDatatable', {
templateUrl: 'app/docker/components/datatables/networks-datatable/networksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<'
}
});

View File

@@ -0,0 +1,97 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Id')">
Id
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Slot')">
Slot
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Slot' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Spec.ContainerSpec.Image')">
Image
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Spec.ContainerSpec.Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Spec.ContainerSpec.Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Updated')">
Last Update
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
<td>{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.Spec.ContainerSpec.Image | hideshasum }}</td>
<td>{{ item.Updated | getisodate }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No task available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,13 @@
angular.module('portainer.docker').component('nodeTasksDatatable', {
templateUrl: 'app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<'
}
});

View File

@@ -0,0 +1,116 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Hostname')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Hostname' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Hostname' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Role')">
Role
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CPUs')">
CPU
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPUs' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPUs' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Memory')">
Memory
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('EngineVersion')">
Engine
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EngineVersion' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EngineVersion' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showIpAddressColumn">
<a ng-click="$ctrl.changeOrderBy('Addr')">
IP Address
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Addr' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Addr' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Hostname }}</span>
</td>
<td>{{ item.Role }}</td>
<td>{{ item.CPUs / 1000000000 }}</td>
<td>{{ item.Memory | humansize }}</td>
<td>{{ item.EngineVersion }}</td>
<td ng-if="$ctrl.showIpAddressColumn">{{ item.Addr }}</td>
<td><span class="label label-{{ item.Status | nodestatusbadge }}">{{ item.Status }}</span></td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="7" class="text-center text-muted">No node available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('nodesDatatable', {
templateUrl: 'app/docker/components/datatables/nodes-datatable/nodesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showIpAddressColumn: '<',
accessToNodeDetails: '<'
}
});

View File

@@ -0,0 +1,105 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('cpu')">
CPU
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'cpu' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'cpu' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('memory')">
Memory
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'memory' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'memory' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ip')">
IP Address
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ip' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ip' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('version')">
Engine
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'version' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'version' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('status')">
Status
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>{{ item.name }}</td>
<td>{{ item.cpu }}</td>
<td>{{ item.memory }}</td>
<td>{{ item.ip }}</td>
<td>{{ item.version }}</td>
<td><span class="label label-{{ item.status | nodestatusbadge }}">{{ item.status }}</span></td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No node available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,13 @@
angular.module('portainer.docker').component('nodesSsDatatable', {
templateUrl: 'app/docker/components/datatables/nodes-ss-datatable/nodesSSDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<'
}
});

View File

@@ -0,0 +1,108 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.secrets.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add secret
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreatedAt')">
Creation Date
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.secrets.secret({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.CreatedAt | getisodate }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No secret available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('secretsDatatable', {
templateUrl: 'app/docker/components/datatables/secrets-datatable/secretsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<'
}
});

View File

@@ -0,0 +1,163 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<div class="btn-group" role="group" aria-label="...">
<button ng-if="$ctrl.showForceUpdateButton" type="button" class="btn btn-sm btn-primary"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.forceUpdateAction($ctrl.state.selectedItems)">
<i class="fa fa-refresh space-right" aria-hidden="true"></i>Update
</button>
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Mode')">
Scheduling Mode
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mode' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mode' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Ports')">
Published Ports
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('UpdatedAt')">
Last Update
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'UpdatedAt' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'UpdatedAt' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.services.service({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Image | hideshasum }}</td>
<td>
{{ item.Mode }} <code>{{ item.Running }}</code> / <code>{{ item.Replicas }}</code>
<span ng-if="item.Mode === 'replicated' && !item.Scale">
<a class="interactive" ng-click="item.Scale = true; item.ReplicaCount = item.Replicas;">
<i class="fa fa-arrows-v" aria-hidden="true"></i> Scale
</a>
</span>
<span ng-if="item.Mode === 'replicated' && item.Scale">
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" />
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
<td>
<a ng-if="item.Ports && item.Ports.length > 0 && $ctrl.swarmManagerIp && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.swarmManagerIp }}:{{ p.PublishedPort }}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!item.Ports || item.Ports.length === 0 || !$ctrl.swarmManagerIp" >-</span>
</td>
<td>{{ item.UpdatedAt | getisodate }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="7" class="text-center text-muted">No service available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,19 @@
angular.module('portainer.docker').component('servicesDatatable', {
templateUrl: 'app/docker/components/datatables/services-datatable/servicesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<',
scaleAction: '<',
swarmManagerIp: '<',
forceUpdateAction: '<',
showForceUpdateButton: '<'
}
});

View File

@@ -0,0 +1,97 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Id')">
Id
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showSlotColumn">
<a ng-click="$ctrl.changeOrderBy('Slot')">
Slot
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Slot' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('NodeId')">
Node
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeId' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeId' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Updated')">
Last Update
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
<td>{{ item.Updated | getisodate }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No task available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('tasksDatatable', {
templateUrl: 'app/docker/components/datatables/tasks-datatable/tasksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
nodes: '<',
showTextFilter: '<',
showSlotColumn: '<'
}
});

View File

@@ -0,0 +1,149 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.volumes.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
<thead>
<tr>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Id')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
<div>
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.usage.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.usage.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
</div>
<div class="dropdown-menu" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Filter by usage
</div>
<div class="menuContent">
<div class="md-checkbox">
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUsedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
<label for="filter_usage_usedImages">Used volumes</label>
</div>
<div class="md-checkbox">
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUnusedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
<label for="filter_usage_unusedImages">Unused volumes</label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.usage.open = false;">Close</a>
</div>
</div>
</div>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Driver')">
Driver
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Driver' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Driver' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Mountpoint')">
Mount point
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="docker.volumes.volume({id: item.Id})" class="monospaced">{{ item.Id | truncate:25 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="item.dangling">Unused</span>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Driver }}</td>
<td>{{ item.Mountpoint | truncatelr }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No volume available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<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>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,15 @@
angular.module('portainer.docker').component('volumesDatatable', {
templateUrl: 'app/docker/components/datatables/volumes-datatable/volumesDatatable.html',
controller: 'VolumesDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<'
}
});

View File

@@ -0,0 +1,102 @@
angular.module('portainer.docker')
.controller('VolumesDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
var ctrl = this;
this.state = {
selectAll: false,
orderBy: this.orderBy,
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
displayTextFilter: false,
selectedItemCount: 0,
selectedItems: []
};
this.filters = {
usage: {
open: false,
enabled: false,
showUsedVolumes: true,
showUnusedVolumes: true
}
};
this.changeOrderBy = function(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
};
this.selectItem = function(item) {
if (item.Checked) {
this.state.selectedItemCount++;
this.state.selectedItems.push(item);
} else {
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
this.state.selectedItemCount--;
}
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.selectItem(item);
}
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
this.updateDisplayTextFilter = function() {
this.state.displayTextFilter = !this.state.displayTextFilter;
if (!this.state.displayTextFilter) {
delete this.state.textFilter;
}
};
this.applyFilters = function(value, index, array) {
var volume = value;
var filters = ctrl.filters;
if ((volume.dangling && filters.usage.showUnusedVolumes)
|| (!volume.dangling && filters.usage.showUsedVolumes)) {
return true;
}
return false;
};
this.onUsageFilterChange = function() {
var filters = this.filters.usage;
var filtered = false;
if (!filters.showUsedVolumes || !filters.showUnusedVolumes) {
filtered = true;
}
this.filters.usage.enabled = filtered;
DatatableService.setDataTableFilters(this.tableKey, this.filters);
};
this.$onInit = function() {
setDefaults(this);
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
this.filters.usage.open = false;
};
function setDefaults(ctrl) {
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
}
}]);

View File

@@ -0,0 +1,12 @@
angular.module('portainer.docker').component('dockerSidebarContent', {
templateUrl: 'app/docker/components/dockerSidebarContent/dockerSidebarContent.html',
bindings: {
'endpointApiVersion': '<',
'swarmManagement': '<',
'standaloneManagement': '<',
'adminAccess': '<',
'externalContributions': '<',
'sidebarToggledOn': '<',
'currentState': '<'
}
});

View File

@@ -0,0 +1,42 @@
<li class="sidebar-list">
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.sidebarToggledOn && $ctrl.externalContributions && ($ctrl.currentState === 'docker.templates' || $ctrl.currentState === 'docker.templates.linuxserver')">
<a ui-sref="docker.templates.linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
<a ui-sref="docker.stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.30 && $ctrl.swarmManagement">
<a ui-sref="docker.configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
<a ui-sref="docker.secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess">
<a ui-sref="docker.events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
</li>

View File

@@ -0,0 +1,9 @@
angular.module('portainer.docker').component('porImageRegistry', {
templateUrl: 'app/docker/components/imageRegistry/porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
'image': '=',
'registry': '=',
'autoComplete': '<'
}
});

View File

@@ -1,12 +1,14 @@
<div>
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
<input type="text" class="form-control" uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
Registry
</label>
<div class="col-sm-10 col-md-4 margin-sm-top">
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry" class="form-control"></select>
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry"
class="form-control"></select>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
angular.module('portainer.docker')
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
var ctrl = this;
function initComponent() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub(),
availableImages: ctrl.autoComplete ? ImageService.images() : []
})
.then(function success(data) {
var dockerhub = data.dockerhub;
var registries = data.registries;
ctrl.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
ctrl.availableRegistries = [dockerhub].concat(registries);
if (!ctrl.registry.Id) {
ctrl.registry = dockerhub;
} else {
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
initComponent();
}]);

View File

@@ -4,39 +4,7 @@ function includeString(text, values) {
});
}
angular.module('portainer.filters', [])
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
if (isNaN(length)) {
length = 10;
}
if (end === undefined) {
end = '...';
}
if (text.length <= length || text.length - end.length <= length) {
return text;
} else {
return String(text).substring(0, length - end.length) + end;
}
};
})
.filter('truncatelr', function () {
'use strict';
return function (text, max, left, right) {
max = isNaN(max) ? 50 : max;
left = isNaN(left) ? 25 : left;
right = isNaN(right) ? 25 : right;
if (text.length <= max) {
return text;
} else {
return text.substring(0, left) + '[...]' + text.substring(text.length - right, text.length);
}
};
})
angular.module('portainer.docker')
.filter('visualizerTask', function () {
'use strict';
return function (text) {
@@ -55,16 +23,19 @@ angular.module('portainer.filters', [])
'use strict';
return function (text) {
var status = _.toLower(text);
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted'])) {
return 'info';
var labelStyle = 'default';
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) {
labelStyle = 'info';
} else if (includeString(status, ['pending'])) {
return 'warning';
} else if (includeString(status, ['shutdown', 'failed', 'rejected'])) {
return 'danger';
labelStyle = 'warning';
} else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) {
labelStyle = 'danger';
} else if (includeString(status, ['complete'])) {
return 'primary';
labelStyle = 'primary';
} else if (includeString(status, ['running'])) {
labelStyle = 'success';
}
return 'success';
return labelStyle;
};
})
.filter('containerstatusbadge', function () {
@@ -121,12 +92,6 @@ angular.module('portainer.filters', [])
return '';
};
})
.filter('capitalize', function () {
'use strict';
return function (text) {
return _.capitalize(text);
};
})
.filter('getstatetext', function () {
'use strict';
return function (state) {
@@ -151,12 +116,6 @@ angular.module('portainer.filters', [])
return 'Stopped';
};
})
.filter('stripprotocol', function() {
'use strict';
return function (url) {
return url.replace(/.*?:\/\//g, '');
};
})
.filter('getstatelabel', function () {
'use strict';
return function (state) {
@@ -172,17 +131,6 @@ angular.module('portainer.filters', [])
return 'label-default';
};
})
.filter('humansize', function () {
'use strict';
return function (bytes, round) {
if (!round) {
round = 1;
}
if (bytes || bytes === 0) {
return filesize(bytes, {base: 10, round: round});
}
};
})
.filter('containername', function () {
'use strict';
return function (container) {
@@ -221,18 +169,6 @@ angular.module('portainer.filters', [])
return [];
};
})
.filter('getisodatefromtimestamp', function () {
'use strict';
return function (timestamp) {
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('getisodate', function () {
'use strict';
return function (date) {
return moment(date).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('command', function () {
'use strict';
return function (command) {
@@ -241,39 +177,6 @@ angular.module('portainer.filters', [])
}
};
})
.filter('key', function () {
'use strict';
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
};
})
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
};
})
.filter('ipaddress', function () {
'use strict';
return function (ip) {
return ip.slice(0, ip.indexOf('/'));
};
})
.filter('arraytostr', function () {
'use strict';
return function (arr, separator) {
if (arr) {
return _.join(arr, separator);
}
return '';
};
})
.filter('hideshasum', function () {
'use strict';
return function (imageName) {
@@ -283,21 +186,6 @@ angular.module('portainer.filters', [])
return '';
};
})
.filter('ownershipicon', function () {
'use strict';
return function (ownership) {
switch (ownership) {
case 'private':
return 'fa fa-eye-slash';
case 'administrators':
return 'fa fa-eye-slash';
case 'restricted':
return 'fa fa-users';
default:
return 'fa fa-eye';
}
};
})
.filter('availablenodecount', function () {
'use strict';
return function (nodes) {
@@ -339,4 +227,13 @@ angular.module('portainer.filters', [])
return function (createdBy) {
return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN ');
};
})
.filter('trimshasum', function () {
'use strict';
return function (imageName) {
if (imageName.indexOf('sha256:') === 0) {
return imageName.substring(7, 19);
}
return _.split(imageName, '@sha256')[0];
};
});

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ConfigHelper', [function ConfigHelperFactory() {
'use strict';
return {

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
var helper = {};

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ImageHelper', [function ImageHelperFactory() {
'use strict';

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('InfoHelper', [function InfoHelperFactory() {
'use strict';
return {

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('LabelHelper', [function LabelHelperFactory() {
'use strict';
return {

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('NodeHelper', [function NodeHelperFactory() {
'use strict';
return {

View File

@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('SecretHelper', [function SecretHelperFactory() {
'use strict';
return {

View File

@@ -1,4 +1,5 @@
angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHelperFactory() {
angular.module('portainer.docker')
.factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
var helper = {};
@@ -141,5 +142,106 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
}
};
helper.translateHumanDurationToNanos = function(humanDuration) {
var nanos;
var regex = /^([0-9]+)(h|m|s|ms|us|ns)$/i;
var matches = humanDuration.match(regex);
if (matches !== null && matches.length === 3) {
var duration = parseInt(matches[1], 10);
var unit = matches[2];
// Moment.js cannot use micro or nanoseconds
switch (unit) {
case 'ns':
nanos = duration;
break;
case 'us':
nanos = duration * 1000;
break;
default:
nanos = moment.duration(duration, unit).asMilliseconds() * 1000000;
}
}
return nanos;
};
// Convert nanoseconds to the higher unit possible
// e.g 1840 nanoseconds = 1804ns
// e.g 300000000000 nanoseconds = 5m
// e.g 3510000000000 nanoseconds = 3510s
// e.g 3540000000000 nanoseconds = 59m
// e.g 3600000000000 nanoseconds = 1h
helper.translateNanosToHumanDuration = function(nanos) {
var humanDuration = '0s';
var conversionFromNano = {};
conversionFromNano['ns'] = 1;
conversionFromNano['us'] = conversionFromNano['ns'] * 1000;
conversionFromNano['ms'] = conversionFromNano['us'] * 1000;
conversionFromNano['s'] = conversionFromNano['ms'] * 1000;
conversionFromNano['m'] = conversionFromNano['s'] * 60;
conversionFromNano['h'] = conversionFromNano['m'] * 60;
Object.keys(conversionFromNano).forEach(function(unit) {
if ( nanos % conversionFromNano[unit] === 0 && (nanos / conversionFromNano[unit]) > 0) {
humanDuration = (nanos / conversionFromNano[unit]) + unit;
}
});
return humanDuration;
};
helper.translateLogDriverOptsToKeyValue = function(logOptions) {
var options = [];
if (logOptions) {
Object.keys(logOptions).forEach(function(key) {
options.push({
key: key,
value: logOptions[key],
originalKey: key,
originalValue: logOptions[key],
added: true
});
});
}
return options;
};
helper.translateKeyValueToLogDriverOpts = function(keyValueLogDriverOpts) {
var options = {};
if (keyValueLogDriverOpts) {
keyValueLogDriverOpts.forEach(function(option) {
if (option.key && option.key !== '' && option.value && option.value !== '') {
options[option.key] = option.value;
}
});
}
return options;
};
helper.translateHostsEntriesToHostnameIP = function(entries) {
var ipHostEntries = [];
if (entries) {
entries.forEach(function(entry) {
if (entry.indexOf(' ') && entry.split(' ').length === 2) {
var keyValue = entry.split(' ');
ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0]});
}
});
}
return ipHostEntries;
};
helper.translateHostnameIPToHostsEntries = function(entries) {
var ipHostEntries = [];
if (entries) {
entries.forEach(function(entry) {
if (entry.ip && entry.hostname) {
ipHostEntries.push(entry.ip + ' ' + entry.hostname);
}
});
}
return ipHostEntries;
};
return helper;
}]);

View File

@@ -0,0 +1,30 @@
angular.module('portainer.docker')
.factory('VolumeHelper', [function VolumeHelperFactory() {
'use strict';
var helper = {};
helper.createDriverOptions = function(optionArray) {
var options = {};
optionArray.forEach(function (option) {
options[option.name] = option.value;
});
return options;
};
helper.isVolumeUsedByAService = function(volume, services) {
for (var i = 0; i < services.length; i++) {
var service = services[i];
var mounts = service.Mounts;
for (var j = 0; j < mounts.length; j++) {
var mount = mounts[j];
if (mount.Source === volume.Id) {
return true;
}
}
}
return false;
};
return helper;
}]);

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