Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7afeb8a80d | |||
| f8ced03792 | |||
| 1fdf56372b | |||
| 835b273700 | |||
| fcc9203416 | |||
| e25c5a014c | |||
| fa9ba303aa | |||
| e6dee37af0 | |||
| d03e992b4f | |||
| 1a868be6ea | |||
| e2fc8af87a | |||
| 70933d1056 | |||
| 7e0b0a05de | |||
| 980f65a08a | |||
| 8cf6d34362 | |||
| 70f139514f | |||
| fa4ec04c47 | |||
| 7ebe4af77d | |||
| 579241db92 | |||
| 7d78871eee | |||
| 3a6e9d2fbe | |||
| e4d98082dc | |||
| cd26051144 | |||
| 27e584fc14 | |||
| 2bdc9322de | |||
| 35d5d75966 | |||
| 2610e3d02a | |||
| d579f62fa7 | |||
| d1b9820a29 | |||
| 13943c3d8b | |||
| d8b800ddbc | |||
| 59f1a2f673 | |||
| 9ee652c818 | |||
| 816c1ea448 | |||
| 0bacaef71a | |||
| 2ef821f118 | |||
| 487cb4e755 | |||
| 06d3debf38 | |||
| 907f83aaff | |||
| 4b747a78cd | |||
| d6f3dd8cda | |||
| 51632e367c | |||
| 6e98237419 | |||
| ecc8857a32 | |||
| 7d05e81c37 | |||
| 6ce3fe7a9e |
@@ -37,6 +37,7 @@ Any other info e.g. Why do you consider this to be a bug? What did you expect to
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Portainer Docker image tag (latest/arm/windows...):
|
||||
* Target Docker version (the host/cluster you manage):
|
||||
* Target Swarm version (if applicable):
|
||||
* Platform (windows/linux):
|
||||
|
||||
@@ -3,3 +3,4 @@ bower_components
|
||||
dist
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
.tmp
|
||||
|
||||
@@ -44,7 +44,7 @@ func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.En
|
||||
|
||||
// Endpoints return an array containing all the endpoints.
|
||||
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints []portainer.Endpoint
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
|
||||
@@ -160,3 +160,15 @@ func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteActive deletes the active endpoint.
|
||||
func (service *EndpointService) DeleteActive() error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
err := bucket.Delete(internal.Itob(activeEndpointID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+8
-8
@@ -25,14 +25,14 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String(),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// +build !windows
|
||||
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\ProgramData\\Portainer"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\ProgramData\\Portainer\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\ProgramData\\Portainer\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\ProgramData\\Portainer\\certs\\key.pem"
|
||||
)
|
||||
@@ -29,6 +29,11 @@ func main() {
|
||||
Logo: *flags.Logo,
|
||||
}
|
||||
|
||||
fileService, err := file.NewService(*flags.Data, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var store = bolt.NewStore(*flags.Data)
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
@@ -41,11 +46,6 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fileService, err := file.NewService(*flags.Data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||
|
||||
// Initialize the active endpoint from the CLI only if there is no
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ const (
|
||||
|
||||
// User errors.
|
||||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
)
|
||||
|
||||
// Endpoint errors.
|
||||
|
||||
+28
-11
@@ -1,13 +1,12 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,18 +20,28 @@ const (
|
||||
TLSKeyFile = "key.pem"
|
||||
)
|
||||
|
||||
// Service represents a service for managing files.
|
||||
// Service represents a service for managing files and directories.
|
||||
type Service struct {
|
||||
dataStorePath string
|
||||
fileStorePath string
|
||||
}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(fileStorePath string) (*Service, error) {
|
||||
// NewService initializes a new service. It creates a data directory and a directory to store files
|
||||
// inside this directory if they don't exist.
|
||||
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
service := &Service{
|
||||
fileStorePath: fileStorePath,
|
||||
dataStorePath: dataStorePath,
|
||||
fileStorePath: path.Join(dataStorePath, fileStorePath),
|
||||
}
|
||||
|
||||
err := service.createFolderInStoreIfNotExist(TLSStorePath)
|
||||
// Checking if a mount directory exists is broken with Go on Windows.
|
||||
// This will need to be reviewed after the issue has been fixed in Go.
|
||||
// err := createDirectoryIfNotExist(dataStorePath, 0755)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,7 +53,7 @@ func NewService(fileStorePath string) (*Service, error) {
|
||||
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointStorePath := path.Join(TLSStorePath, ID)
|
||||
err := service.createFolderInStoreIfNotExist(endpointStorePath)
|
||||
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,12 +106,20 @@ func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// createFolderInStoreIfNotExist creates a new folder in the file store if it doesn't exists on the file system.
|
||||
func (service *Service) createFolderInStoreIfNotExist(name string) error {
|
||||
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
|
||||
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
return createDirectoryIfNotExist(path, 0700)
|
||||
}
|
||||
|
||||
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
|
||||
func createDirectoryIfNotExist(path string, mode uint32) error {
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
os.Mkdir(path, 0600)
|
||||
err = os.Mkdir(path, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ type unixSocketHandler struct {
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
@@ -143,7 +143,7 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
@@ -154,6 +154,6 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ type putEndpointsRequest struct {
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
// DELETE /endpoints/0 deletes the active endpoint
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
@@ -270,7 +271,14 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
endpointID = int(endpoint.ID)
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
}
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
@@ -284,6 +292,13 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if id == "0" {
|
||||
err = handler.EndpointService.DeleteActive()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func newFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func isHTML(acceptContent []string) bool {
|
||||
for _, accept := range acceptContent {
|
||||
if strings.Contains(accept, "text/html") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
fileHandler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
+4
-2
@@ -19,7 +19,7 @@ type Handler struct {
|
||||
DockerHandler *DockerHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
FileHandler http.Handler
|
||||
FileHandler *FileHandler
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -55,7 +55,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Error writes an API error message to the response and logger.
|
||||
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||
// Log error.
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
if logger != nil {
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
}
|
||||
|
||||
// Write generic error response.
|
||||
w.WriteHeader(code)
|
||||
|
||||
@@ -47,13 +47,13 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err := service.jwtService.VerifyToken(token)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
Error(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ func (server *Server) Start() error {
|
||||
endpointHandler.server = server
|
||||
var uploadHandler = NewUploadHandler(middleWareService)
|
||||
uploadHandler.FileService = server.FileService
|
||||
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
|
||||
var fileHandler = newFileHandler(server.AssetsPath)
|
||||
|
||||
server.Handler = &Handler{
|
||||
AuthHandler: authHandler,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -40,15 +39,13 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
|
||||
|
||||
resp, err := http.Get(handler.templatesURL)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
http.Error(w, fmt.Sprintf("Error making request to %s: %s", handler.templatesURL, err.Error()), http.StatusInternalServerError)
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUploadTLS(w, r)
|
||||
})))
|
||||
return h
|
||||
|
||||
+20
-10
@@ -227,18 +227,28 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
user, err := handler.UserService.User("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -94,6 +94,7 @@ type (
|
||||
DeleteEndpoint(ID EndpointID) error
|
||||
GetActive() (*Endpoint, error)
|
||||
SetActive(endpoint *Endpoint) error
|
||||
DeleteActive() error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data.
|
||||
@@ -118,7 +119,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of portainer API.
|
||||
APIVersion = "1.11.0"
|
||||
APIVersion = "1.11.2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+30
-16
@@ -36,6 +36,7 @@ angular.module('portainer', [
|
||||
'swarm',
|
||||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'createNetwork',
|
||||
'task',
|
||||
'templates',
|
||||
@@ -49,8 +50,8 @@ angular.module('portainer', [
|
||||
.setPrefix('portainer');
|
||||
|
||||
jwtOptionsProvider.config({
|
||||
tokenGetter: ['localStorageService', function(localStorageService) {
|
||||
return localStorageService.get('JWT');
|
||||
tokenGetter: ['LocalStorage', function(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
}],
|
||||
unauthenticatedRedirector: ['$state', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
@@ -398,6 +399,22 @@ angular.module('portainer', [
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('node', {
|
||||
url: '^/nodes/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/node/node.html',
|
||||
controller: 'NodeController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
@@ -528,30 +545,27 @@ angular.module('portainer', [
|
||||
};
|
||||
});
|
||||
}])
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'EndpointMode', function ($rootScope, $state, Authentication, authManager, EndpointMode) {
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', function ($rootScope, $state, Authentication, authManager, StateManager) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
|
||||
Authentication.init();
|
||||
StateManager.init();
|
||||
|
||||
$rootScope.$state = $state;
|
||||
|
||||
$rootScope.$on('tokenHasExpired', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
});
|
||||
|
||||
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
|
||||
if (toState.name !== 'endpointInit' && (fromState.name === 'auth' || fromState.name === '' || fromState.name === 'endpointInit') && Authentication.isAuthenticated()) {
|
||||
EndpointMode.determineEndpointMode();
|
||||
}
|
||||
});
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_ENDPOINT', '/api/docker')
|
||||
.constant('CONFIG_ENDPOINT', '/api/settings')
|
||||
.constant('AUTH_ENDPOINT', '/api/auth')
|
||||
.constant('USERS_ENDPOINT', '/api/users')
|
||||
.constant('ENDPOINTS_ENDPOINT', '/api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
||||
.constant('DOCKER_ENDPOINT', 'api/docker')
|
||||
.constant('CONFIG_ENDPOINT', 'api/settings')
|
||||
.constant('AUTH_ENDPOINT', 'api/auth')
|
||||
.constant('USERS_ENDPOINT', 'api/users')
|
||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.11.0');
|
||||
.constant('UI_VERSION', 'v1.11.2');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'Messages',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, Messages) {
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
@@ -60,7 +60,12 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password).then(function success() {
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}, function error(err) {
|
||||
if (err.status === 404) {
|
||||
$state.go('endpointInit');
|
||||
|
||||
@@ -174,6 +174,23 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.HostConfig.RestartPolicy.Name !== 'no'">
|
||||
<td>Restart policies</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr>
|
||||
<td class="col-md-3">Name</td>
|
||||
<td>{{ container.HostConfig.RestartPolicy.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-md-3">MaximumRetryCount</td>
|
||||
<td>
|
||||
{{ container.HostConfig.RestartPolicy.MaximumRetryCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
@@ -208,7 +225,18 @@
|
||||
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Connected networks"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-sitemap" title="Connected networks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -219,7 +247,7 @@
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: pagination_count">
|
||||
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: state.pagination_count">
|
||||
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
|
||||
<td>{{ value.IPAddress || '-' }}</td>
|
||||
<td>{{ value.Gateway || '-' }}</td>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Settings',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Settings) {
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('container_networks');
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('container_networks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var update = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
@@ -75,8 +80,8 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
|
||||
$scope.commit = function () {
|
||||
$('#createImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
$('#createImageSpinner').hide();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Containers</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -11,7 +12,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-server" title="Containers">
|
||||
<div class="pull-right">
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -37,7 +45,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Status')">
|
||||
State
|
||||
@@ -66,7 +76,7 @@
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<th ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
Host IP
|
||||
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
@@ -75,7 +85,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Ports')">
|
||||
Exposed 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>
|
||||
@@ -83,14 +93,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="endpointMode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
<td>
|
||||
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
|
||||
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config) {
|
||||
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination',
|
||||
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var update = function (data) {
|
||||
$('#loadContainersSpinner').show();
|
||||
$scope.state.selectedItemCount = 0;
|
||||
@@ -28,7 +32,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
||||
if (model.IP) {
|
||||
$scope.state.displayIP = true;
|
||||
}
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
|
||||
}
|
||||
return model;
|
||||
@@ -77,6 +81,19 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else if (action === Container.pause) {
|
||||
action({id: c.Id}, function (d) {
|
||||
if (d.message) {
|
||||
Messages.send("Container is already paused", c.Id);
|
||||
} else {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to pause container');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else {
|
||||
action({id: c.Id}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
@@ -94,6 +111,15 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
||||
}
|
||||
};
|
||||
|
||||
$scope.selectItems = function (allSelected) {
|
||||
angular.forEach($scope.state.filteredContainers, function (container) {
|
||||
if (container.Checked !== allSelected) {
|
||||
container.Checked = allSelected;
|
||||
$scope.selectItem(container);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
@@ -152,7 +178,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
|
||||
@@ -72,7 +72,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
@@ -141,7 +141,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||
}
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = _.toLower(config.Image);
|
||||
var image = config.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
@@ -220,7 +220,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||
var containerName = container;
|
||||
if (container && typeof container === 'object') {
|
||||
containerName = $filter('trimcontainername')(container.Names[0]);
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
containerName = $filter('swarmcontainername')(container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +95,6 @@
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<label for="container_labels" class="col-sm-1 control-label text-left">Labels</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -269,7 +259,7 @@
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider !== 'DOCKER_SWARM_MODE'">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
@@ -289,10 +279,10 @@
|
||||
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
|
||||
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
|
||||
<div class="col-sm-9">
|
||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider !== 'DOCKER_SWARM'">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
@@ -33,7 +33,7 @@
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
@@ -60,7 +60,7 @@
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
||||
@@ -19,7 +19,8 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
|
||||
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
|
||||
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
|
||||
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
|
||||
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey).then(function success(data) {
|
||||
var type = $scope.endpointType;
|
||||
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) {
|
||||
Messages.send("Endpoint updated", $scope.endpoint.Name);
|
||||
$state.go('endpoints');
|
||||
}, function error(err) {
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
<!-- endpoin-type radio -->
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType">Manage the Docker instance where Portainer is running</label>
|
||||
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="cleanError()">Manage the Docker instance where Portainer is running</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType">Manage a remote Docker instance</label>
|
||||
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="cleanError()">Manage a remote Docker instance</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- endpoint-type radio -->
|
||||
<!-- local-endpoint -->
|
||||
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: ensure that the Docker socket is bind mounted in the Portainer container at <code>/var/run/docker.sock</code></span>
|
||||
</div>
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
|
||||
<span class="small text-primary">This feature is not available with Docker <b>on</b> Windows yet.</span>
|
||||
<div class="small text-primary">On Linux / Mac, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
|
||||
</div>
|
||||
<!-- connect button -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
@@ -41,7 +41,10 @@
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
@@ -122,7 +125,10 @@
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('endpointInit', [])
|
||||
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'Messages',
|
||||
function ($scope, $state, EndpointService, Messages) {
|
||||
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, EndpointService, StateManager, Messages) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
@@ -15,27 +15,39 @@ function ($scope, $state, EndpointService, Messages) {
|
||||
TLSKey: null
|
||||
};
|
||||
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
if (err.status !== 404) {
|
||||
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.cleanError = function() {
|
||||
$scope.state.error = '';
|
||||
};
|
||||
|
||||
$scope.createLocalEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = "local";
|
||||
var URL = "unix:///var/run/docker.sock";
|
||||
var TLS = false;
|
||||
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
StateManager.updateEndpointState(false)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
EndpointService.deleteEndpoint(0)
|
||||
.then(function success() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to connect to the Docker endpoint';
|
||||
});
|
||||
});
|
||||
}, function error(err) {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to create endpoint';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRemoteEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
@@ -43,9 +55,20 @@ function ($scope, $state, EndpointService, Messages) {
|
||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||
var TLSCertFile = $scope.formValues.TLSCert;
|
||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true).then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
|
||||
.then(function success(data) {
|
||||
StateManager.updateEndpointState(false)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
EndpointService.deleteEndpoint(0)
|
||||
.then(function success() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to connect to the Docker endpoint';
|
||||
});
|
||||
});
|
||||
}, function error(err) {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = err.msg;
|
||||
}, function update(evt) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="endpoints" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadEndpointsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Endpoint management</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -101,7 +102,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title="Endpoints">
|
||||
<div class="pull-right">
|
||||
<i id="loadEndpointsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -143,7 +151,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
|
||||
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
|
||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
angular.module('endpoints', [])
|
||||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Settings', 'Messages',
|
||||
function ($scope, $state, EndpointService, Settings, Messages) {
|
||||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination',
|
||||
function ($scope, $state, EndpointService, Messages, Pagination) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false,
|
||||
selectedItemCount: 0
|
||||
selectedItemCount: 0,
|
||||
pagination_count: Pagination.getPaginationCount('endpoints')
|
||||
};
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -24,6 +24,10 @@ function ($scope, $state, EndpointService, Settings, Messages) {
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadEventsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Events</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -12,7 +13,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-history" title="Events">
|
||||
<div class="pull-right">
|
||||
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -49,7 +57,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
|
||||
<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>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
angular.module('events', [])
|
||||
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
|
||||
function ($scope, Settings, Messages, Events) {
|
||||
.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination',
|
||||
function ($scope, Messages, Events, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('events');
|
||||
$scope.sortType = 'Time';
|
||||
$scope.sortReverse = true;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('events', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var from = moment().subtract(24, 'hour').unix();
|
||||
var to = moment().unix();
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
|
||||
|
||||
$scope.tagImage = function() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
Messages.send('Image successfully tagged');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="images" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Images</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -50,7 +51,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Images">
|
||||
<div class="pull-right">
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -66,7 +74,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="images" ng-click="order('Id')">
|
||||
Id
|
||||
@@ -98,7 +108,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
||||
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
|
||||
<td>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
angular.module('images', [])
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Settings',
|
||||
function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('images');
|
||||
$scope.sortType = 'RepoTags';
|
||||
$scope.sortReverse = true;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
@@ -17,6 +17,19 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
|
||||
$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++;
|
||||
@@ -27,8 +40,8 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
|
||||
|
||||
$scope.pullImage = function() {
|
||||
$('#pullImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('main', [])
|
||||
.controller('MainController', ['$scope', '$cookieStore',
|
||||
function ($scope, $cookieStore) {
|
||||
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
|
||||
function ($scope, $cookieStore, StateManager) {
|
||||
|
||||
/**
|
||||
* Sidebar Toggle & Cookie Control
|
||||
@@ -10,6 +10,8 @@ function ($scope, $cookieStore) {
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
$scope.applicationState = StateManager.getState();
|
||||
|
||||
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
|
||||
if (newValue >= mobileView) {
|
||||
if (angular.isDefined($cookieStore.get('toggle'))) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('network', [])
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Container', 'ContainerHelper', 'Messages',
|
||||
function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Messages) {
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) {
|
||||
|
||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
@@ -38,34 +38,63 @@ function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Mes
|
||||
|
||||
function getNetwork() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function (d) {
|
||||
$scope.network = d;
|
||||
getContainersInNetwork(d);
|
||||
Network.get({id: $stateParams.id}, function success(data) {
|
||||
$scope.network = data;
|
||||
getContainersInNetwork(data);
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve network info");
|
||||
Messages.error("Failure", err, "Unable to retrieve network info");
|
||||
});
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(network, containers) {
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
|
||||
}
|
||||
var containersInNetwork = [];
|
||||
containers.forEach(function(container) {
|
||||
var containerInNetwork = network.Containers[container.Id];
|
||||
containerInNetwork.Id = container.Id;
|
||||
// Name is not available in Docker 1.9
|
||||
if (!containerInNetwork.Name) {
|
||||
containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
|
||||
}
|
||||
containersInNetwork.push(containerInNetwork);
|
||||
});
|
||||
$scope.containersInNetwork = containersInNetwork;
|
||||
}
|
||||
|
||||
function getContainersInNetwork(network) {
|
||||
if (network.Containers) {
|
||||
Container.query({
|
||||
filters: {network: [$stateParams.id]}
|
||||
}, function (containersInNetworkResult) {
|
||||
if ($scope.containersToHideLabels) {
|
||||
containersInNetworkResult = ContainerHelper.hideContainers(containersInNetworkResult, $scope.containersToHideLabels);
|
||||
}
|
||||
var containersInNetwork = [];
|
||||
containersInNetworkResult.forEach(function(container) {
|
||||
var containerInNetwork = network.Containers[container.Id];
|
||||
containerInNetwork.Id = container.Id;
|
||||
containersInNetwork.push(containerInNetwork);
|
||||
if ($scope.applicationState.endpoint.apiVersion < 1.24) {
|
||||
Container.query({}, function success(data) {
|
||||
var containersInNetwork = data.filter(function filter(container) {
|
||||
if (container.HostConfig.NetworkMode === network.Name) {
|
||||
return container;
|
||||
}
|
||||
});
|
||||
filterContainersInNetwork(network, containersInNetwork);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve containers in network");
|
||||
});
|
||||
$scope.containersInNetwork = containersInNetwork;
|
||||
});
|
||||
} else {
|
||||
Container.query({
|
||||
filters: {network: [$stateParams.id]}
|
||||
}, function success(data) {
|
||||
filterContainersInNetwork(network, data);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve containers in network");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getNetwork();
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
getNetwork();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Networks</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -23,12 +24,12 @@
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM' || endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
|
||||
</div>
|
||||
@@ -52,7 +53,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
<div class="pull-right">
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -68,7 +76,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
@@ -121,7 +131,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
|
||||
<td>{{ network.Id }}</td>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
angular.module('networks', [])
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Settings',
|
||||
function ($scope, $state, Network, Config, Messages, Settings) {
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Network, Config, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.advancedSettings = false;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
function prepareNetworkConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
config.Driver = 'overlay';
|
||||
// Force IPAM Driver to 'default', should not be required.
|
||||
// See: https://github.com/docker/docker/issues/25735
|
||||
@@ -47,6 +51,15 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
||||
$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++;
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Node details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="node({id: node.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="swarm">Swarm nodes</a> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="!node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div ng-if="loading">
|
||||
<i class="fa fa-cog fa-spin"></i> Loading...
|
||||
</div>
|
||||
|
||||
<rd-widget ng-if="!loading">
|
||||
<rd-widget-header icon="fa-object-group" title="Node does not exist"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<p>It looks like the node you wish to inspect does not exist.</p>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Node specification"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<input type="text" class="input-sm" ng-model="node.Name" placeholder="e.g. my-manager" ng-change="updateNodeAttribute(node, 'Name')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Host name</td>
|
||||
<td>{{ node.Hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>{{ node.Role }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Availability</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="nodeAvailability" class="selectpicker form-control" ng-model="node.Availability" ng-change="updateNodeAttribute(node, 'Availability')">
|
||||
<option value="active">Active</option>
|
||||
<option value="pause">Pause</option>
|
||||
<option value="drain">Drain</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span class="label label-{{ node.Status|nodestatusbadge }}">{{ node.Status }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<p class="small text-muted">
|
||||
View the Docker Swarm mode Node documentation <a href="https://docs.docker.com/engine/swarm/manage-nodes/" target="self">here</a>.
|
||||
</p>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(node, ['Name', 'Availability'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && node.Role === 'manager'">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Manager status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Leader</td>
|
||||
<td>
|
||||
<span ng-if="node.Leader"><i class="fa fa-check green-icon" aria-hidden="true"></i> Yes</span>
|
||||
<span ng-if="!node.Leader"><i class="fa fa-times red-icon" aria-hidden="true"></i> No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reachability</td>
|
||||
<td>{{ node.Reachability }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manager address</td>
|
||||
<td>{{ node.ManagerAddr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Node description"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU</td>
|
||||
<td>{{ node.CPUs / 1000000000 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory</td>
|
||||
<td>{{ node.Memory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>{{ node.PlatformOS }} {{ node.PlatformArchitecture }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Engine version</td>
|
||||
<td>{{ node.EngineVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Node labels">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="addLabel(node)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!node.Labels || node.Labels.length === 0">
|
||||
<p>There are no labels for this node.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body classes="no-padding" ng-if="node.Labels && node.Labels.length > 0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in node.Labels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(node, label)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel(node, $index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(node, ['Labels'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && tasks.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Slot')">
|
||||
Slot
|
||||
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Image')">
|
||||
Image
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Updated')">
|
||||
Last update
|
||||
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
|
||||
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
<td>{{ task.Image }}</td>
|
||||
<td>{{ task.Updated|getisodate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="tasks" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
angular.module('node', [])
|
||||
.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Messages',
|
||||
function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Messages) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('node_tasks');
|
||||
$scope.loading = true;
|
||||
$scope.tasks = [];
|
||||
$scope.displayNode = false;
|
||||
$scope.sortType = 'Status';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
var originalNode = {};
|
||||
var editedKeys = [];
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('node_tasks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.updateNodeAttribute = function updateNodeAttribute(node, key) {
|
||||
editedKeys.push(key);
|
||||
};
|
||||
$scope.addLabel = function addLabel(node) {
|
||||
node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' });
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
};
|
||||
$scope.removeLabel = function removeLabel(node, index) {
|
||||
var removedElement = node.Labels.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
$scope.updateLabel = function updateLabel(node, label) {
|
||||
if (label.value !== label.originalValue || label.key !== label.originalKey) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.hasChanges = function(node, elements) {
|
||||
if (!elements) {
|
||||
elements = Object.keys(originalNode);
|
||||
}
|
||||
var hasChanges = false;
|
||||
elements.forEach(function(key) {
|
||||
hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]);
|
||||
});
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function(node) {
|
||||
editedKeys.forEach(function(key) {
|
||||
node[key] = originalNode[key];
|
||||
});
|
||||
editedKeys = [];
|
||||
};
|
||||
|
||||
$scope.updateNode = function updateNode(node) {
|
||||
var config = NodeHelper.nodeToConfig(node.Model);
|
||||
config.Name = node.Name;
|
||||
config.Availability = node.Availability;
|
||||
config.Role = node.Role;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels);
|
||||
|
||||
Node.update({ id: node.Id, version: node.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Node successfully updated", "Node updated");
|
||||
$state.go('node', {id: node.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.error("Failure", e, "Failed to update node");
|
||||
});
|
||||
};
|
||||
|
||||
function loadNodeAndTasks() {
|
||||
$scope.loading = true;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
Node.get({ id: $stateParams.id}, function(d) {
|
||||
if (d.message) {
|
||||
Messages.error("Failure", e, "Unable to inspect the node");
|
||||
} else {
|
||||
var node = new NodeViewModel(d);
|
||||
originalNode = angular.copy(node);
|
||||
$scope.node = node;
|
||||
getTasks(d);
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
} else {
|
||||
$scope.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTasks(node) {
|
||||
if (node) {
|
||||
Task.query({filters: {node: [node.ID]}}, function (tasks) {
|
||||
$scope.tasks = tasks.map(function (task) {
|
||||
return new TaskViewModel(task, [node]);
|
||||
});
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve tasks associated to the node");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeAndTasks();
|
||||
|
||||
}]);
|
||||
@@ -200,17 +200,14 @@
|
||||
<td>Update Failure Action</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
|
||||
Continue
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
|
||||
Pause
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-8"></div>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
|
||||
Continue
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
|
||||
Pause
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -230,7 +227,18 @@
|
||||
<div class="row" ng-if="tasks.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -267,7 +275,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
|
||||
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
angular.module('service', [])
|
||||
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Settings',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Settings) {
|
||||
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
||||
$scope.service = {};
|
||||
$scope.tasks = [];
|
||||
$scope.displayNode = false;
|
||||
$scope.sortType = 'Status';
|
||||
$scope.sortReverse = false;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
var previousServiceValues = {};
|
||||
|
||||
@@ -16,6 +17,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('service_tasks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.renameService = function renameService(service) {
|
||||
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
|
||||
service.EditName = false;
|
||||
@@ -25,7 +30,8 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
|
||||
service.EditImage = false;
|
||||
};
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
updateServiceAttribute(service, 'Replicas', service.newServiceReplicas || service.Replicas);
|
||||
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
|
||||
updateServiceAttribute(service, 'Replicas', replicas);
|
||||
service.EditReplicas = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadServicesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Services</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -12,7 +13,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Services">
|
||||
<div class="pull-right">
|
||||
<i id="loadServicesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -52,7 +60,7 @@
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
|
||||
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
|
||||
<td>{{ service.Image }}</td>
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
angular.module('services', [])
|
||||
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Settings',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settings) {
|
||||
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('services');
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('services', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
$('#loadServicesSpinner').show();
|
||||
@@ -23,19 +40,6 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settin
|
||||
});
|
||||
};
|
||||
|
||||
$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.removeAction = function () {
|
||||
$('#loadServicesSpinner').show();
|
||||
var counter = 0;
|
||||
|
||||
@@ -17,41 +17,41 @@
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||
<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">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
|
||||
<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">Images <span class="menu-icon fa fa-clone"></span></a>
|
||||
<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">Networks <span class="menu-icon fa fa-sitemap"></span></a>
|
||||
<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">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
||||
<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="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM' || (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER')">
|
||||
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||
<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="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Portainer settings</span></li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="settings">Password <span class="menu-icon fa fa-lock"></span></a>
|
||||
<a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="endpoints">Endpoints <span class="menu-icon fa fa-plug"></span></a>
|
||||
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('sidebar', [])
|
||||
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'EndpointMode', 'Messages',
|
||||
function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messages) {
|
||||
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
@@ -10,8 +10,12 @@ function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messa
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
EndpointService.setActive(endpoint.Id).then(function success(data) {
|
||||
EndpointMode.determineEndpointMode();
|
||||
$state.reload();
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.reload();
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
|
||||
});
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, "Unable to switch to new endpoint");
|
||||
});
|
||||
|
||||
@@ -54,7 +54,18 @@
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
@@ -69,7 +80,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
|
||||
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
angular.module('stats', [])
|
||||
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
||||
function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
||||
.controller('StatsController', ['Pagination', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
||||
function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
||||
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
|
||||
// TODO: Force memory scale to 0 - max memory
|
||||
$scope.ps_args = '';
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
||||
$scope.sortType = 'CMD';
|
||||
$scope.sortReverse = false;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
||||
};
|
||||
$scope.getTop = function () {
|
||||
ContainerTop.get($stateParams.id, {
|
||||
ps_args: $scope.ps_args
|
||||
@@ -114,6 +117,12 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
});
|
||||
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
|
||||
|
||||
function setUpdateStatsTimeout() {
|
||||
if(!destroyed) {
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
Container.stats({id: $stateParams.id}, function (d) {
|
||||
var arr = Object.keys(d).map(function (key) {
|
||||
@@ -129,15 +138,17 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
updateCpuChart(d);
|
||||
updateMemoryChart(d);
|
||||
updateNetworkChart(d);
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
setUpdateStatsTimeout();
|
||||
}, function () {
|
||||
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
setUpdateStatsTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
var destroyed = false;
|
||||
var timeout;
|
||||
$scope.$on('$destroy', function () {
|
||||
destroyed = true;
|
||||
$timeout.cancel(timeout);
|
||||
});
|
||||
|
||||
@@ -162,16 +173,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
$scope.networkName = Object.keys(data.networks)[0];
|
||||
data.network = data.networks[$scope.networkName];
|
||||
}
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
if(data.network) {
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
|
||||
function calculateCPUPercent(stats) {
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Swarm version</td>
|
||||
<td>{{ docker.Version|swarmversion }}</td>
|
||||
</tr>
|
||||
@@ -31,29 +31,29 @@
|
||||
<td>Docker API version</td>
|
||||
<td>{{ docker.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<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="endpointMode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</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="endpointMode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
|
||||
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</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="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Operating system</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Kernel version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Go version</td>
|
||||
<td>{{ docker.GoVersion }}</td>
|
||||
</tr>
|
||||
@@ -65,18 +65,29 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<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"></rd-widget-header>
|
||||
<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')">
|
||||
<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>
|
||||
<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>
|
||||
@@ -94,30 +105,30 @@
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('IP')">
|
||||
<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>
|
||||
<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('Engine')">
|
||||
<a ui-sref="swarm" ng-click="order('version')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<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')">
|
||||
<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>
|
||||
<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: pagination_count))">
|
||||
<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>
|
||||
@@ -133,60 +144,71 @@
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<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"></rd-widget-header>
|
||||
<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')">
|
||||
<a ui-sref="swarm" ng-click="order('Description.Hostname')">
|
||||
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>
|
||||
<span ng-show="sortType == 'Description.Hostname' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Hostname' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('type')">
|
||||
<a ui-sref="swarm" ng-click="order('Spec.Role')">
|
||||
Role
|
||||
<span ng-show="sortType == 'type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'Spec.Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Spec.Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('cpu')">
|
||||
<a ui-sref="swarm" ng-click="order('Description.Resources.NanoCPUs')">
|
||||
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>
|
||||
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('memory')">
|
||||
<a ui-sref="swarm" ng-click="order('Description.Resources.MemoryBytes')">
|
||||
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>
|
||||
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Engine')">
|
||||
<a ui-sref="swarm" ng-click="order('Description.Engine.EngineVersion')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'Description.Engine.EngineVersion' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Engine.EngineVersion' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Status')">
|
||||
<a ui-sref="swarm" ng-click="order('node.Status.State')">
|
||||
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>
|
||||
<span ng-show="sortType == 'node.Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'node.Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<td>{{ node.Description.Hostname }}</td>
|
||||
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="node({id: node.ID})">{{ node.Description.Hostname }}</a></td>
|
||||
<td>{{ node.Spec.Role }}</td>
|
||||
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
|
||||
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
angular.module('swarm', [])
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Settings',
|
||||
function ($scope, Info, Version, Node, Settings) {
|
||||
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination',
|
||||
function ($scope, Info, Version, Node, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
|
||||
$scope.sortType = 'Spec.Role';
|
||||
$scope.sortReverse = false;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$scope.totalCPU = 0;
|
||||
$scope.totalMemory = 0;
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
Node.query({}, function(d) {
|
||||
$scope.nodes = d;
|
||||
var CPU = 0, memory = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Templates</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -13,12 +14,12 @@
|
||||
</rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider === 'DOCKER_SWARM'">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
||||
@@ -41,10 +42,10 @@
|
||||
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
@@ -106,21 +107,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="state.selectedTemplate">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-rocket" title="Available templates">
|
||||
<div class="pull-right">
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="template-list">
|
||||
<div dir-paginate="tpl in templates | itemsPerPage: pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
||||
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
||||
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||
<div class="title">{{ tpl.title }}</div>
|
||||
<div class="description">{{ tpl.description }}</div>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
angular.module('templates', [])
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Settings',
|
||||
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Settings) {
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false
|
||||
showAdvancedOptions: false,
|
||||
pagination_count: Pagination.getPaginationCount('templates')
|
||||
};
|
||||
$scope.formValues = {
|
||||
network: "",
|
||||
name: "",
|
||||
ports: []
|
||||
};
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
var selectedItem = -1;
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
@@ -115,7 +119,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
||||
if (v.value || v.set) {
|
||||
var val;
|
||||
if (v.type && v.type === 'container') {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
|
||||
val = $filter('swarmcontainername')(v.value);
|
||||
} else {
|
||||
var container = v.value;
|
||||
@@ -134,7 +138,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
||||
}
|
||||
|
||||
function prepareImageConfig(config, template) {
|
||||
var image = _.toLower(template.image);
|
||||
var image = template.image;
|
||||
var registry = template.registry || '';
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
@@ -206,7 +210,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Volumes</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -11,7 +12,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cubes" title="Volumes">
|
||||
<div class="pull-right">
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
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">
|
||||
@@ -28,7 +36,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="volumes" ng-click="order('Name')">
|
||||
Name
|
||||
@@ -53,7 +63,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
|
||||
<td>{{ volume.Name|truncate:50 }}</td>
|
||||
<td>{{ volume.Driver }}</td>
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
angular.module('volumes', [])
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Settings',
|
||||
function ($scope, $state, Volume, Messages, Settings) {
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Volume, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('volumes');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
$scope.pagination_count = Settings.pagination_count;
|
||||
|
||||
$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++;
|
||||
|
||||
@@ -67,7 +67,7 @@ angular.module('portainer.filters', [])
|
||||
.filter('nodestatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
if (text === 'Unhealthy') {
|
||||
if (text === 'down' || text === 'Unhealthy') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
angular.module('portainer.helpers', [])
|
||||
.factory('InfoHelper', [function InfoHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
determineEndpointMode: function(info) {
|
||||
var mode = {
|
||||
provider: '',
|
||||
role: ''
|
||||
};
|
||||
if (_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
mode.provider = "DOCKER_SWARM";
|
||||
if (info.SystemStatus[0][1] === 'primary') {
|
||||
mode.role = "PRIMARY";
|
||||
} else {
|
||||
mode.role = "REPLICA";
|
||||
}
|
||||
} else {
|
||||
if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) {
|
||||
mode.provider = "DOCKER_STANDALONE";
|
||||
} else {
|
||||
mode.provider = "DOCKER_SWARM_MODE";
|
||||
if (info.Swarm.ControlAvailable) {
|
||||
mode.role = "MANAGER";
|
||||
} else {
|
||||
mode.role = "WORKER";
|
||||
}
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('LabelHelper', [function LabelHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
fromLabelHashToKeyValue: function(labels) {
|
||||
if (labels) {
|
||||
return Object.keys(labels).map(function(key) {
|
||||
return {key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
},
|
||||
fromKeyValueToLabelHash: function(labelKV) {
|
||||
var labels = {};
|
||||
if (labelKV) {
|
||||
labelKV.forEach(function(label) {
|
||||
labels[label.key] = label.value;
|
||||
});
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('ImageHelper', [function ImageHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
@@ -63,6 +116,19 @@ angular.module('portainer.helpers', [])
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('NodeHelper', [function NodeHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
nodeToConfig: function(node) {
|
||||
return {
|
||||
Name: node.Spec.Name,
|
||||
Role: node.Spec.Role,
|
||||
Labels: node.Spec.Labels,
|
||||
Availability: node.Spec.Availability
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('TemplateHelper', [function TemplateHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
|
||||
@@ -56,7 +56,7 @@ function deleteImageHandler(data) {
|
||||
response.push({message: data});
|
||||
}
|
||||
// A JSON object is returned on failure (Docker = 1.12)
|
||||
else if (!isJSONArray) {
|
||||
else if (!isJSONArray(data)) {
|
||||
var json = angular.fromJson(data);
|
||||
response.push(json);
|
||||
}
|
||||
|
||||
+104
-45
@@ -153,10 +153,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
.factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-7-nodes
|
||||
return $resource(Settings.url + '/nodes', {}, {
|
||||
query: {
|
||||
method: 'GET', isArray: true
|
||||
}
|
||||
return $resource(Settings.url + '/nodes/:id/:action', {}, {
|
||||
query: {method: 'GET', isArray: true},
|
||||
get: {method: 'GET', params: {id: '@id'}},
|
||||
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
||||
remove: { method: 'DELETE', params: {id: '@id'} }
|
||||
});
|
||||
}])
|
||||
.factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) {
|
||||
@@ -240,44 +241,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } }
|
||||
});
|
||||
}])
|
||||
.factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) {
|
||||
'use strict';
|
||||
return {
|
||||
determineEndpointMode: function() {
|
||||
Info.get({}, function(d) {
|
||||
var mode = {
|
||||
provider: '',
|
||||
role: ''
|
||||
};
|
||||
if (_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
mode.provider = "DOCKER_SWARM";
|
||||
if (d.SystemStatus[0][1] === 'primary') {
|
||||
mode.role = "PRIMARY";
|
||||
} else {
|
||||
mode.role = "REPLICA";
|
||||
}
|
||||
} else {
|
||||
if (!d.Swarm || _.isEmpty(d.Swarm.NodeID)) {
|
||||
mode.provider = "DOCKER_STANDALONE";
|
||||
} else {
|
||||
mode.provider = "DOCKER_SWARM_MODE";
|
||||
if (d.Swarm.ControlAvailable) {
|
||||
mode.role = "MANAGER";
|
||||
} else {
|
||||
mode.role = "WORKER";
|
||||
}
|
||||
}
|
||||
}
|
||||
$rootScope.endpointMode = mode;
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'localStorageService', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, localStorageService) {
|
||||
.factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, LocalStorage, StateManager) {
|
||||
'use strict';
|
||||
return {
|
||||
init: function() {
|
||||
var jwt = localStorageService.get('JWT');
|
||||
var jwt = LocalStorage.getJWT();
|
||||
if (jwt) {
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
$rootScope.username = tokenPayload.username;
|
||||
@@ -287,7 +255,7 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
return $q(function (resolve, reject) {
|
||||
Auth.login({username: username, password: password}).$promise
|
||||
.then(function(data) {
|
||||
localStorageService.set('JWT', data.jwt);
|
||||
LocalStorage.storeJWT(data.jwt);
|
||||
$rootScope.username = username;
|
||||
resolve();
|
||||
}, function() {
|
||||
@@ -296,10 +264,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
localStorageService.remove('JWT');
|
||||
StateManager.clean();
|
||||
LocalStorage.clean();
|
||||
},
|
||||
isAuthenticated: function() {
|
||||
var jwt = localStorageService.get('JWT');
|
||||
var jwt = LocalStorage.getJWT();
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
}
|
||||
};
|
||||
@@ -359,7 +328,97 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
setActiveEndpoint: { method: 'POST', params: { id: '@id', action: 'active' } }
|
||||
});
|
||||
}])
|
||||
.factory('EndpointService', ['$q', '$timeout', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, $timeout, Endpoints, FileUploadService) {
|
||||
.factory('Pagination', ['LocalStorage', 'Settings', function PaginationFactory(LocalStorage, Settings) {
|
||||
'use strict';
|
||||
return {
|
||||
getPaginationCount: function(key) {
|
||||
var storedCount = LocalStorage.getPaginationCount(key);
|
||||
var paginationCount = Settings.pagination_count;
|
||||
if (storedCount !== null) {
|
||||
paginationCount = storedCount;
|
||||
}
|
||||
return '' + paginationCount;
|
||||
},
|
||||
setPaginationCount: function(key, count) {
|
||||
LocalStorage.storePaginationCount(key, count);
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('LocalStorage', ['localStorageService', function LocalStorageFactory(localStorageService) {
|
||||
'use strict';
|
||||
return {
|
||||
storeEndpointState: function(state) {
|
||||
localStorageService.set('ENDPOINT_STATE', state);
|
||||
},
|
||||
getEndpointState: function() {
|
||||
return localStorageService.get('ENDPOINT_STATE');
|
||||
},
|
||||
storeJWT: function(jwt) {
|
||||
localStorageService.set('JWT', jwt);
|
||||
},
|
||||
getJWT: function() {
|
||||
return localStorageService.get('JWT');
|
||||
},
|
||||
deleteJWT: function() {
|
||||
localStorageService.remove('JWT');
|
||||
},
|
||||
storePaginationCount: function(key, count) {
|
||||
localStorageService.cookie.set('pagination_' + key, count);
|
||||
},
|
||||
getPaginationCount: function(key) {
|
||||
return localStorageService.cookie.get('pagination_' + key);
|
||||
},
|
||||
clean: function() {
|
||||
localStorageService.clearAll();
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('StateManager', ['$q', 'Info', 'InfoHelper', 'Version', 'LocalStorage', function StateManagerFactory($q, Info, InfoHelper, Version, LocalStorage) {
|
||||
'use strict';
|
||||
|
||||
var state = {
|
||||
loading: true,
|
||||
application: {},
|
||||
endpoint: {}
|
||||
};
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
var endpointState = LocalStorage.getEndpointState();
|
||||
if (endpointState) {
|
||||
state.endpoint = endpointState;
|
||||
}
|
||||
state.loading = false;
|
||||
},
|
||||
clean: function() {
|
||||
state.endpoint = {};
|
||||
},
|
||||
updateEndpointState: function(loading) {
|
||||
var deferred = $q.defer();
|
||||
if (loading) {
|
||||
state.loading = true;
|
||||
}
|
||||
$q.all([Info.get({}).$promise, Version.get({}).$promise])
|
||||
.then(function success(data) {
|
||||
var endpointMode = InfoHelper.determineEndpointMode(data[0]);
|
||||
var endpointAPIVersion = parseFloat(data[1].ApiVersion);
|
||||
state.endpoint.mode = endpointMode;
|
||||
state.endpoint.apiVersion = endpointAPIVersion;
|
||||
LocalStorage.storeEndpointState(state.endpoint);
|
||||
state.loading = false;
|
||||
deferred.resolve();
|
||||
}, function error(err) {
|
||||
state.loading = false;
|
||||
deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err});
|
||||
});
|
||||
return deferred.promise;
|
||||
},
|
||||
getState: function() {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('EndpointService', ['$q', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||
'use strict';
|
||||
return {
|
||||
getActive: function() {
|
||||
@@ -374,11 +433,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
endpoints: function() {
|
||||
return Endpoints.query({}).$promise;
|
||||
},
|
||||
updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, type) {
|
||||
var endpoint = {
|
||||
id: ID,
|
||||
Name: name,
|
||||
URL: "tcp://" + URL,
|
||||
URL: type === 'local' ? ("unix://" + URL) : ("tcp://" + URL),
|
||||
TLS: TLS
|
||||
};
|
||||
var deferred = $q.defer();
|
||||
|
||||
@@ -14,6 +14,7 @@ function TaskViewModel(data, node_data) {
|
||||
this.Updated = data.UpdatedAt;
|
||||
this.Slot = data.Slot;
|
||||
this.Status = data.Status.State;
|
||||
this.Image = data.Spec.ContainerSpec ? data.Spec.ContainerSpec.Image : '';
|
||||
if (node_data) {
|
||||
for (var i = 0; i < node_data.length; ++i) {
|
||||
if (data.NodeID === node_data[i].ID) {
|
||||
@@ -60,6 +61,42 @@ function ServiceViewModel(data) {
|
||||
this.EditName = false;
|
||||
}
|
||||
|
||||
function NodeViewModel(data) {
|
||||
this.Model = data;
|
||||
this.Id = data.ID;
|
||||
this.Version = data.Version.Index;
|
||||
this.Name = data.Spec.Name;
|
||||
this.Role = data.Spec.Role;
|
||||
this.CreatedAt = data.CreatedAt;
|
||||
this.UpdatedAt = data.UpdatedAt;
|
||||
this.Availability = data.Spec.Availability;
|
||||
|
||||
var labels = data.Spec.Labels;
|
||||
if (labels) {
|
||||
this.Labels = Object.keys(labels).map(function(key) {
|
||||
return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true };
|
||||
});
|
||||
} else {
|
||||
this.Labels = [];
|
||||
}
|
||||
|
||||
this.Hostname = data.Description.Hostname;
|
||||
this.PlatformArchitecture = data.Description.Platform.Architecture;
|
||||
this.PlatformOS = data.Description.Platform.OS;
|
||||
this.CPUs = data.Description.Resources.NanoCPUs;
|
||||
this.Memory = data.Description.Resources.MemoryBytes;
|
||||
this.EngineVersion = data.Description.Engine.EngineVersion;
|
||||
this.EngineLabels = data.Description.Engine.Labels;
|
||||
this.Plugins = data.Description.Engine.Plugins;
|
||||
this.Status = data.Status.State;
|
||||
|
||||
if (data.ManagerStatus) {
|
||||
this.Leader = data.ManagerStatus.Leader;
|
||||
this.Reachability = data.ManagerStatus.Reachability;
|
||||
this.ManagerAddr = data.ManagerStatus.Addr;
|
||||
}
|
||||
}
|
||||
|
||||
function ContainerViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Status = data.Status;
|
||||
|
||||
+7
-1
@@ -213,7 +213,6 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
margin-top: 25px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -262,3 +261,10 @@ input[type="radio"] {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a.active {
|
||||
color: #fff;
|
||||
text-indent: 22px;
|
||||
border-left: 3px solid #fff;
|
||||
background: #2d3e63;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portainer",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"homepage": "https://github.com/portainer/portainer",
|
||||
"authors": [
|
||||
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
|
||||
|
||||
@@ -27,6 +27,13 @@ cd /tmp/portainer-build-arm
|
||||
tar cvpfz portainer-${VERSION}-linux-arm.tar.gz portainer
|
||||
cd -
|
||||
|
||||
grunt release-arm64
|
||||
rm -rf /tmp/portainer-build-arm64 && mkdir -pv /tmp/portainer-build-arm64/portainer
|
||||
mv dist/* /tmp/portainer-build-arm64/portainer
|
||||
cd /tmp/portainer-build-arm64
|
||||
tar cvpfz portainer-${VERSION}-linux-arm64.tar.gz portainer
|
||||
cd -
|
||||
|
||||
grunt release-macos
|
||||
rm -rf /tmp/portainer-build-darwin && mkdir -pv /tmp/portainer-build-darwin/portainer
|
||||
mv dist/* /tmp/portainer-build-darwin/portainer
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM microsoft/windowsservercore
|
||||
|
||||
COPY dist /
|
||||
|
||||
VOLUME C:\\ProgramData\\Portainer
|
||||
|
||||
WORKDIR /
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM microsoft/nanoserver
|
||||
|
||||
COPY dist /
|
||||
|
||||
VOLUME C:\\ProgramData\\Portainer
|
||||
|
||||
WORKDIR /
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
-390
@@ -1,390 +0,0 @@
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-recess');
|
||||
grunt.loadNpmTasks('grunt-karma');
|
||||
grunt.loadNpmTasks('grunt-html2js');
|
||||
grunt.loadNpmTasks('grunt-shell');
|
||||
grunt.loadNpmTasks('grunt-if');
|
||||
|
||||
// Default task.
|
||||
grunt.registerTask('default', ['jshint', 'build', 'karma:unit']);
|
||||
grunt.registerTask('build', [
|
||||
'clean:app',
|
||||
'if:unixBinaryNotExist',
|
||||
'html2js',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'recess:build',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('release', [
|
||||
'clean:all',
|
||||
'if:unixBinaryNotExist',
|
||||
'html2js',
|
||||
'uglify',
|
||||
'clean:tmpl',
|
||||
'jshint',
|
||||
//'karma:unit',
|
||||
'concat:index',
|
||||
'recess:min',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('release-win', [
|
||||
'clean:all',
|
||||
'if:windowsBinaryNotExist',
|
||||
'html2js',
|
||||
'uglify',
|
||||
'clean:tmpl',
|
||||
'jshint',
|
||||
//'karma:unit',
|
||||
'concat:index',
|
||||
'recess:min',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('release-arm', [
|
||||
'clean:all',
|
||||
'if:unixArmBinaryNotExist',
|
||||
'html2js',
|
||||
'uglify',
|
||||
'clean:tmpl',
|
||||
'jshint',
|
||||
//'karma:unit',
|
||||
'concat:index',
|
||||
'recess:min',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('release-macos', [
|
||||
'clean:all',
|
||||
'if:darwinBinaryNotExist',
|
||||
'html2js',
|
||||
'uglify',
|
||||
'clean:tmpl',
|
||||
'jshint',
|
||||
//'karma:unit',
|
||||
'concat:index',
|
||||
'recess:min',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('lint', ['jshint']);
|
||||
grunt.registerTask('test-watch', ['karma:watch']);
|
||||
grunt.registerTask('run', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
|
||||
grunt.registerTask('run-swarm', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-swarm-local', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarmLocal', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-dev', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
|
||||
grunt.registerTask('run-ssl', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
|
||||
grunt.registerTask('clear', ['clean:app']);
|
||||
|
||||
// Print a timestamp (useful for when watching)
|
||||
grunt.registerTask('timestamp', function () {
|
||||
grunt.log.subhead(Date());
|
||||
});
|
||||
|
||||
var karmaConfig = function (configFile, customOptions) {
|
||||
var options = {configFile: configFile, keepalive: true};
|
||||
var travisOptions = process.env.TRAVIS && {browsers: ['Firefox'], reporters: 'dots'};
|
||||
return grunt.util._.extend(options, customOptions, travisOptions);
|
||||
};
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
distdir: 'dist',
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
remoteApiVersion: 'v1.20',
|
||||
banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
|
||||
'<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' +
|
||||
' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' +
|
||||
' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n',
|
||||
src: {
|
||||
js: ['app/**/*.js', '!app/**/*.spec.js'],
|
||||
jsTpl: ['<%= distdir %>/templates/**/*.js'],
|
||||
jsVendor: [
|
||||
'bower_components/jquery/dist/jquery.min.js',
|
||||
'bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||
'bower_components/Chart.js/Chart.min.js',
|
||||
'bower_components/lodash/dist/lodash.min.js',
|
||||
'bower_components/filesize/lib/filesize.min.js',
|
||||
'bower_components/moment/min/moment.min.js',
|
||||
'bower_components/xterm.js/dist/xterm.js',
|
||||
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
||||
'assets/js/legend.js' // Not a bower package
|
||||
],
|
||||
specs: ['test/**/*.spec.js'],
|
||||
scenarios: ['test/**/*.scenario.js'],
|
||||
html: ['index.html'],
|
||||
tpl: ['app/components/**/*.html'],
|
||||
css: ['assets/css/app.css'],
|
||||
cssVendor: [
|
||||
'bower_components/bootstrap/dist/css/bootstrap.css',
|
||||
'bower_components/jquery.gritter/css/jquery.gritter.css',
|
||||
'bower_components/font-awesome/css/font-awesome.min.css',
|
||||
'bower_components/rdash-ui/dist/css/rdash.min.css',
|
||||
'bower_components/angular-ui-select/dist/select.min.css',
|
||||
'bower_components/xterm.js/dist/xterm.css'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
all: ['<%= distdir %>/*'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
|
||||
tmpl: ['<%= distdir %>/templates']
|
||||
},
|
||||
copy: {
|
||||
assets: {
|
||||
files: [
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/font-awesome/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/rdash-ui/dist/fonts/'},
|
||||
{
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**', '!trees.jpg'],
|
||||
expand: true,
|
||||
cwd: 'bower_components/jquery.gritter/images/'
|
||||
},
|
||||
{
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**'],
|
||||
expand: true,
|
||||
cwd: 'assets/images/'
|
||||
},
|
||||
{dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'}
|
||||
]
|
||||
}
|
||||
},
|
||||
karma: {
|
||||
unit: {options: karmaConfig('test/unit/karma.conf.js')},
|
||||
watch: {options: karmaConfig('test/unit/karma.conf.js', {singleRun: false, autoWatch: true})}
|
||||
},
|
||||
html2js: {
|
||||
app: {
|
||||
options: {
|
||||
base: '.'
|
||||
},
|
||||
src: ['<%= src.tpl %>'],
|
||||
dest: '<%= distdir %>/templates/app.js',
|
||||
module: '<%= pkg.name %>.templates'
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
dist: {
|
||||
options: {
|
||||
banner: "<%= banner %>",
|
||||
process: true
|
||||
},
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
index: {
|
||||
src: ['index.html'],
|
||||
dest: '<%= distdir %>/index.html',
|
||||
options: {
|
||||
process: true
|
||||
}
|
||||
},
|
||||
angular: {
|
||||
src: ['bower_components/angular/angular.min.js',
|
||||
'bower_components/angular-sanitize/angular-sanitize.min.js',
|
||||
'bower_components/angular-cookies/angular-cookies.min.js',
|
||||
'bower_components/angular-local-storage/dist/angular-local-storage.min.js',
|
||||
'bower_components/angular-jwt/dist/angular-jwt.min.js',
|
||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||
'bower_components/angular-resource/angular-resource.min.js',
|
||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||
'bower_components/ng-file-upload/ng-file-upload.min.js',
|
||||
'bower_components/angular-utils-pagination/dirPagination.js',
|
||||
'bower_components/angular-ui-select/dist/select.min.js'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
dist: {
|
||||
options: {
|
||||
banner: "<%= banner %>"
|
||||
},
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
angular: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= concat.angular.src %>'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
recess: { // TODO: not maintained, unable to preserve license comments, switch out for something better.
|
||||
build: {
|
||||
files: {
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
noOverqualifying: false // TODO: Added because of .nav class, rename
|
||||
}
|
||||
},
|
||||
min: {
|
||||
files: {
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
compress: true,
|
||||
noOverqualifying: false // TODO: Added because of .nav class, rename
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
all: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['default', 'timestamp']
|
||||
},
|
||||
build: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:run', 'shell:cleanImages']
|
||||
/*
|
||||
* Why don't we just use a host volume
|
||||
* http.FileServer uses sendFile which virtualbox hates
|
||||
* Tried using a host volume with -v, copying files with `docker cp`, restating container, none worked
|
||||
* Rebuilding image on each change was only method that worked, takes ~4s per change to update
|
||||
*/
|
||||
},
|
||||
buildSwarm: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
|
||||
},
|
||||
buildSsl: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
files: ['gruntFile.js', '<%= src.js %>', '<%= src.specs %>', '<%= src.scenarios %>'],
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
boss: true,
|
||||
eqnull: true,
|
||||
globals: {
|
||||
angular: false,
|
||||
'$': false
|
||||
}
|
||||
}
|
||||
},
|
||||
shell: {
|
||||
buildImage: {
|
||||
command: 'docker build --rm -t portainer -f build/linux/Dockerfile .'
|
||||
},
|
||||
buildBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer dist/'
|
||||
].join(' && ')
|
||||
},
|
||||
buildUnixArmBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildDarwinBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildWindowsBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
|
||||
].join(' && ')
|
||||
},
|
||||
run: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data'
|
||||
].join(';')
|
||||
},
|
||||
runSwarm: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 --swarm -d /data'
|
||||
].join(';')
|
||||
},
|
||||
runSwarmLocal: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer --swarm'
|
||||
].join(';')
|
||||
},
|
||||
runSsl: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify'
|
||||
].join(';')
|
||||
},
|
||||
cleanImages: {
|
||||
command: 'docker rmi $(docker images -q -f dangling=true)'
|
||||
}
|
||||
},
|
||||
'if': {
|
||||
unixBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildBinary']
|
||||
},
|
||||
unixArmBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildUnixArmBinary']
|
||||
},
|
||||
darwinBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildDarwinBinary']
|
||||
},
|
||||
windowsBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer.exe'
|
||||
},
|
||||
ifFalse: ['shell:buildWindowsBinary']
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
module.exports = function (grunt) {
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-recess');
|
||||
grunt.loadNpmTasks('grunt-html2js');
|
||||
grunt.loadNpmTasks('grunt-shell');
|
||||
grunt.loadNpmTasks('grunt-if');
|
||||
grunt.loadNpmTasks('grunt-filerev');
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
||||
grunt.loadNpmTasks('grunt-usemin');
|
||||
|
||||
// Default task.
|
||||
grunt.registerTask('default', ['jshint', 'build']);
|
||||
grunt.registerTask('build', [
|
||||
'clean:app',
|
||||
'if:unixBinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare:dev',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'copy',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('release', [
|
||||
'clean:all',
|
||||
'if:unixBinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare:release',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy:assets',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('release-win', [
|
||||
'clean:all',
|
||||
'if:windowsBinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('release-arm', [
|
||||
'clean:all',
|
||||
'if:unixArmBinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('release-arm64', [
|
||||
'clean:all',
|
||||
'if:unixArm64BinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('release-macos', [
|
||||
'clean:all',
|
||||
'if:darwinBinaryNotExist',
|
||||
'html2js',
|
||||
'useminPrepare',
|
||||
'recess:build',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'copy',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'clean:tmp'
|
||||
]);
|
||||
grunt.registerTask('lint', ['jshint']);
|
||||
grunt.registerTask('run', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
|
||||
grunt.registerTask('run-swarm', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-swarm-local', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarmLocal', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-dev', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
|
||||
grunt.registerTask('run-ssl', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
|
||||
grunt.registerTask('clear', ['clean:app']);
|
||||
|
||||
// Print a timestamp (useful for when watching)
|
||||
grunt.registerTask('timestamp', function () {
|
||||
grunt.log.subhead(Date());
|
||||
});
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
distdir: 'dist',
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
src: {
|
||||
js: ['app/**/*.js', '!app/**/*.spec.js'],
|
||||
jsTpl: ['<%= distdir %>/templates/**/*.js'],
|
||||
jsVendor: [
|
||||
'bower_components/jquery/dist/jquery.min.js',
|
||||
'bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||
'bower_components/Chart.js/Chart.min.js',
|
||||
'bower_components/lodash/dist/lodash.min.js',
|
||||
'bower_components/filesize/lib/filesize.min.js',
|
||||
'bower_components/moment/min/moment.min.js',
|
||||
'bower_components/xterm.js/dist/xterm.js',
|
||||
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
||||
'assets/js/legend.js' // Not a bower package
|
||||
],
|
||||
html: ['index.html'],
|
||||
tpl: ['app/components/**/*.html'],
|
||||
css: ['assets/css/app.css'],
|
||||
cssVendor: [
|
||||
'bower_components/bootstrap/dist/css/bootstrap.css',
|
||||
'bower_components/jquery.gritter/css/jquery.gritter.css',
|
||||
'bower_components/font-awesome/css/font-awesome.min.css',
|
||||
'bower_components/rdash-ui/dist/css/rdash.min.css',
|
||||
'bower_components/angular-ui-select/dist/select.min.css',
|
||||
'bower_components/xterm.js/dist/xterm.css'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
all: ['<%= distdir %>/*'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
|
||||
tmpl: ['<%= distdir %>/templates'],
|
||||
tmp: ['<%= distdir %>/js/*', '!<%= distdir %>/js/app.*.js', '<%= distdir %>/css/*', '!<%= distdir %>/css/app.*.css']
|
||||
},
|
||||
useminPrepare: {
|
||||
dev: {
|
||||
src: '<%= src.html %>',
|
||||
options: {
|
||||
root: '<%= distdir %>',
|
||||
flow: {
|
||||
steps: {
|
||||
js: ['concat'],
|
||||
css: ['concat']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
release: {
|
||||
src: '<%= src.html %>',
|
||||
options: {
|
||||
root: '<%= distdir %>'
|
||||
}
|
||||
}
|
||||
},
|
||||
filerev: {
|
||||
files: {
|
||||
src: ['<%= distdir %>/js/*.js', '<%= distdir %>/css/*.css']
|
||||
}
|
||||
},
|
||||
usemin: {
|
||||
html: ['<%= distdir %>/index.html']
|
||||
},
|
||||
copy: {
|
||||
bundle: {
|
||||
files: [
|
||||
{
|
||||
dest: '<%= distdir %>/js/',
|
||||
src: ['app.js'],
|
||||
expand: true,
|
||||
cwd: '.tmp/concat/js/'
|
||||
},
|
||||
{
|
||||
dest: '<%= distdir %>/css/',
|
||||
src: ['app.css'],
|
||||
expand: true,
|
||||
cwd: '.tmp/concat/css/'
|
||||
}
|
||||
]
|
||||
},
|
||||
assets: {
|
||||
files: [
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/font-awesome/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/rdash-ui/dist/fonts/'},
|
||||
{
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**', '!trees.jpg'],
|
||||
expand: true,
|
||||
cwd: 'bower_components/jquery.gritter/images/'
|
||||
},
|
||||
{
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**'],
|
||||
expand: true,
|
||||
cwd: 'assets/images/'
|
||||
},
|
||||
{dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'}
|
||||
]
|
||||
}
|
||||
},
|
||||
html2js: {
|
||||
app: {
|
||||
options: {
|
||||
base: '.'
|
||||
},
|
||||
src: ['<%= src.tpl %>'],
|
||||
dest: '<%= distdir %>/templates/app.js',
|
||||
module: '<%= pkg.name %>.templates'
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
dist: {
|
||||
options: {
|
||||
process: true
|
||||
},
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
index: {
|
||||
src: ['index.html'],
|
||||
dest: '<%= distdir %>/index.html',
|
||||
options: {
|
||||
process: true
|
||||
}
|
||||
},
|
||||
angular: {
|
||||
src: ['bower_components/angular/angular.min.js',
|
||||
'bower_components/angular-sanitize/angular-sanitize.min.js',
|
||||
'bower_components/angular-cookies/angular-cookies.min.js',
|
||||
'bower_components/angular-local-storage/dist/angular-local-storage.min.js',
|
||||
'bower_components/angular-jwt/dist/angular-jwt.min.js',
|
||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||
'bower_components/angular-resource/angular-resource.min.js',
|
||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||
'bower_components/ng-file-upload/ng-file-upload.min.js',
|
||||
'bower_components/angular-utils-pagination/dirPagination.js',
|
||||
'bower_components/angular-ui-select/dist/select.min.js'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
dist: {
|
||||
// options: {
|
||||
// },
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
angular: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= concat.angular.src %>'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
recess: { // TODO: not maintained, unable to preserve license comments, switch out for something better.
|
||||
build: {
|
||||
files: {
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
noOverqualifying: false // TODO: Added because of .nav class, rename
|
||||
}
|
||||
},
|
||||
min: {
|
||||
files: {
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
compress: true,
|
||||
noOverqualifying: false // TODO: Added because of .nav class, rename
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
all: {
|
||||
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['default', 'timestamp']
|
||||
},
|
||||
build: {
|
||||
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:run', 'shell:cleanImages']
|
||||
/*
|
||||
* Why don't we just use a host volume
|
||||
* http.FileServer uses sendFile which virtualbox hates
|
||||
* Tried using a host volume with -v, copying files with `docker cp`, restating container, none worked
|
||||
* Rebuilding image on each change was only method that worked, takes ~4s per change to update
|
||||
*/
|
||||
},
|
||||
buildSwarm: {
|
||||
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
|
||||
},
|
||||
buildSsl: {
|
||||
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
files: ['gruntfile.js', '<%= src.js %>'],
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
boss: true,
|
||||
eqnull: true,
|
||||
globals: {
|
||||
angular: false,
|
||||
'$': false
|
||||
}
|
||||
}
|
||||
},
|
||||
shell: {
|
||||
buildImage: {
|
||||
command: 'docker build --rm -t portainer -f build/linux/Dockerfile .'
|
||||
},
|
||||
buildBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer dist/'
|
||||
].join(' && ')
|
||||
},
|
||||
buildUnixArmBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildUnixArm64Binary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-linux-arm64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-linux-arm64 dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildDarwinBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildWindowsBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
|
||||
].join(' && ')
|
||||
},
|
||||
run: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer'
|
||||
].join(';')
|
||||
},
|
||||
runSwarm: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 --name portainer portainer -H tcp://10.0.7.10:2375'
|
||||
].join(';')
|
||||
},
|
||||
runSwarmLocal: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer'
|
||||
].join(';')
|
||||
},
|
||||
runSsl: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 --tlsverify'
|
||||
].join(';')
|
||||
},
|
||||
cleanImages: {
|
||||
command: 'docker rmi $(docker images -q -f dangling=true)'
|
||||
}
|
||||
},
|
||||
'if': {
|
||||
unixBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildBinary']
|
||||
},
|
||||
unixArmBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildUnixArmBinary']
|
||||
},
|
||||
unixArm64BinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildUnixArm64Binary']
|
||||
},
|
||||
darwinBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildDarwinBinary']
|
||||
},
|
||||
windowsBinaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/portainer.exe'
|
||||
},
|
||||
ifFalse: ['shell:buildWindowsBinary']
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
+30
-5
@@ -7,17 +7,21 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="<%= pkg.author %>">
|
||||
|
||||
<!-- build:css css/app.css -->
|
||||
<link href="css/vendor.css" rel="stylesheet">
|
||||
<link href="css/<%= pkg.name %>.css" rel="stylesheet">
|
||||
<link href="css/portainer.css" rel="stylesheet">
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- build:js js/app.js -->
|
||||
<script src="js/angular.js"></script>
|
||||
<script src="js/vendor.js"></script>
|
||||
<script src="js/<%= pkg.name %>.js"></script>
|
||||
<script src="js/portainer.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- Fav and touch icons -->
|
||||
<link rel="shortcut icon" href="ico/favicon.ico">
|
||||
@@ -25,15 +29,36 @@
|
||||
</head>
|
||||
|
||||
<body ng-controller="MainController">
|
||||
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth' && $state.current.name !== 'endpointInit', nopadding: $state.current.name === 'auth' || $state.current.name === 'endpointInit'}" ng-cloak>
|
||||
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth' && $state.current.name !== 'endpointInit' && $state.current.name !== 'init', nopadding: $state.current.name === 'auth' || $state.current.name === 'endpointInit' || $state.current.name === 'init' || applicationState.loading }" ng-cloak>
|
||||
|
||||
<div id="sideview" ui-view="sidebar"></div>
|
||||
<div id="sideview" ui-view="sidebar" ng-if="!applicationState.loading"></div>
|
||||
|
||||
<div id="content-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="page-wrapper" ng-if="applicationState.loading">
|
||||
<!-- loading box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||
<!-- loading box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
</div>
|
||||
<!-- !loading box logo -->
|
||||
<!-- panel -->
|
||||
<div class="row" style="text-align: center">
|
||||
Connecting to the Docker enpoint...
|
||||
<i class="fa fa-cog fa-spin" style="margin-left: 5px"></i>
|
||||
</div>
|
||||
<!-- !panel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !loading box -->
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="view" ui-view="content"></div>
|
||||
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div>
|
||||
|
||||
</div><!-- End Page Content -->
|
||||
</div><!-- End Content Wrapper -->
|
||||
|
||||
+5
-2
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -26,14 +26,17 @@
|
||||
"grunt-contrib-clean": "~0.4.0",
|
||||
"grunt-contrib-concat": "~0.1.3",
|
||||
"grunt-contrib-copy": "~0.4.0",
|
||||
"grunt-contrib-cssmin": "^1.0.2",
|
||||
"grunt-contrib-jshint": "~0.2.0",
|
||||
"grunt-contrib-uglify": "^0.9.2",
|
||||
"grunt-contrib-watch": "~0.3.1",
|
||||
"grunt-filerev": "^2.3.1",
|
||||
"grunt-html2js": "~0.1.0",
|
||||
"grunt-if": "^0.1.5",
|
||||
"grunt-karma": "~0.4.4",
|
||||
"grunt-recess": "~0.3",
|
||||
"grunt-shell": "^1.1.2"
|
||||
"grunt-shell": "^1.1.2",
|
||||
"grunt-usemin": "^3.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bower install"
|
||||
|
||||
Reference in New Issue
Block a user