mirror of https://github.com/portainer/portainer
commit
c40f120da2
|
@ -31,12 +31,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
kingpin.Version(version)
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
CheckHealth: kingpin.Flag("check-health", "GET http://localhost:<port>/api/health endpoint").Default(defaultCheckHealth).Short('c').Bool(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
|
||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
|
@ -46,6 +46,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
// Deprecated flags
|
||||
|
|
|
@ -6,6 +6,7 @@ const (
|
|||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultCheckHealth = "false"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
|
|
@ -4,6 +4,7 @@ const (
|
|||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultCheckHealth = "false"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
|
|
@ -7,13 +7,14 @@ import (
|
|||
"github.com/portainer/portainer/cron"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/exec"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
"github.com/portainer/portainer/git"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
"github.com/portainer/portainer/ldap"
|
||||
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func initCLI() *portainer.CLIFlags {
|
||||
|
@ -31,7 +32,7 @@ func initCLI() *portainer.CLIFlags {
|
|||
}
|
||||
|
||||
func initFileService(dataStorePath string) portainer.FileService {
|
||||
fileService, err := file.NewService(dataStorePath, "")
|
||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -171,6 +172,19 @@ func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService
|
|||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
if *flags.CheckHealth {
|
||||
statuscode, err := http.HealthCheck(*flags.Addr)
|
||||
if err == nil {
|
||||
if statuscode == 200 {
|
||||
log.Println(*flags.Addr, ": Online - response:", statuscode)
|
||||
os.Exit(0)
|
||||
} else {
|
||||
log.Fatal(*flags.Addr, ": Error - response:", statuscode)
|
||||
}
|
||||
}
|
||||
log.Fatal("Connection error:", err.Error())
|
||||
}
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
store := initStore(*flags.Data)
|
||||
|
|
|
@ -54,10 +54,15 @@ func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
|
|||
}
|
||||
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||
} else {
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||
}
|
||||
|
||||
env := make([]string, 0)
|
||||
for _, envvar := range stack.Env {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package file
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"bytes"
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
|
@ -138,11 +138,11 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
|
|||
}
|
||||
|
||||
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
|
||||
err := handler.FileService.DeleteTLSFiles(file.LDAPStorePath)
|
||||
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter
|
|||
}
|
||||
|
||||
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
@ -37,6 +37,14 @@ type StackHandler struct {
|
|||
StackManager portainer.StackManager
|
||||
}
|
||||
|
||||
type stackDeploymentConfig struct {
|
||||
endpoint *portainer.Endpoint
|
||||
stack *portainer.Stack
|
||||
prune bool
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
}
|
||||
|
||||
// NewStackHandler returns a new instance of StackHandler.
|
||||
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
|
||||
h := &StackHandler{
|
||||
|
@ -78,6 +86,7 @@ type (
|
|||
putStackRequest struct {
|
||||
StackFileContent string `valid:"required"`
|
||||
Env []portainer.Pair `valid:""`
|
||||
Prune bool `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -166,7 +175,7 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
|
|||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: file.ComposeFileDefaultName,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: req.Env,
|
||||
}
|
||||
|
||||
|
@ -207,7 +216,14 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -264,7 +280,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
|
|||
}
|
||||
|
||||
if req.PathInRepository == "" {
|
||||
req.PathInRepository = file.ComposeFileDefaultName
|
||||
req.PathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
|
@ -334,7 +350,14 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -404,7 +427,7 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
|
|||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: file.ComposeFileDefaultName,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: env,
|
||||
}
|
||||
|
||||
|
@ -445,7 +468,14 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: false,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -637,7 +667,14 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
|
||||
config := stackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
prune: req.Prune,
|
||||
}
|
||||
err = handler.deployStack(&config)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
@ -732,22 +769,22 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re
|
|||
}
|
||||
}
|
||||
|
||||
func (handler *StackHandler) deployStack(endpoint *portainer.Endpoint, stack *portainer.Stack, dockerhub *portainer.DockerHub, registries []portainer.Registry) error {
|
||||
func (handler *StackHandler) deployStack(config *stackDeploymentConfig) error {
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
err := handler.StackManager.Login(dockerhub, registries, endpoint)
|
||||
err := handler.StackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
||||
if err != nil {
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.StackManager.Deploy(stack, endpoint)
|
||||
err = handler.StackManager.Deploy(config.stack, config.prune, config.endpoint)
|
||||
if err != nil {
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.StackManager.Logout(endpoint)
|
||||
err = handler.StackManager.Logout(config.endpoint)
|
||||
if err != nil {
|
||||
handler.stackCreationMutex.Unlock()
|
||||
return err
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HealthCheck GETs /api/status
|
||||
func HealthCheck(addr string) (int, error) {
|
||||
resp, err := http.Get("http://" + addr + "/api/status")
|
||||
return resp.StatusCode, err
|
||||
}
|
|
@ -13,10 +13,10 @@ type (
|
|||
CLIFlags struct {
|
||||
Addr *string
|
||||
Assets *string
|
||||
CheckHealth *bool
|
||||
Data *string
|
||||
ExternalEndpoints *string
|
||||
SyncInterval *string
|
||||
Endpoint *string
|
||||
ExternalEndpoints *string
|
||||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
TLSVerify *bool
|
||||
|
@ -26,12 +26,13 @@ type (
|
|||
SSL *bool
|
||||
SSLCert *string
|
||||
SSLKey *string
|
||||
SyncInterval *string
|
||||
AdminPassword *string
|
||||
AdminPasswordFile *string
|
||||
// Deprecated fields
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Templates *string
|
||||
Labels *[]Pair
|
||||
}
|
||||
|
||||
// Status represents the application status.
|
||||
|
@ -383,14 +384,14 @@ type (
|
|||
StackManager interface {
|
||||
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) error
|
||||
Logout(endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.15.5"
|
||||
APIVersion = "1.16.0"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 7
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
|
|
397
api/swagger.yaml
397
api/swagger.yaml
|
@ -56,7 +56,7 @@ info:
|
|||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
|
||||
version: "1.15.5"
|
||||
version: "1.16.0"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
|
@ -77,6 +77,8 @@ tags:
|
|||
description: "Manage Portainer settings"
|
||||
- name: "status"
|
||||
description: "Information about the Portainer instance"
|
||||
- name: "stacks"
|
||||
description: "Manage Docker stacks"
|
||||
- name: "users"
|
||||
description: "Manage users"
|
||||
- name: "teams"
|
||||
|
@ -436,6 +438,285 @@ paths:
|
|||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/endpoints/{endpointId}/stacks:
|
||||
get:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "List stacks"
|
||||
description: |
|
||||
List all stacks based on the current user authorizations.
|
||||
Will return all stacks if using an administrator account otherwise it
|
||||
will only return the list of stacks the user have access to.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/StackListResponse"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Access denied to resource"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
post:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "Deploy a new stack"
|
||||
description: |
|
||||
Deploy a new stack into a Docker environment specified via the endpoint identifier.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "method"
|
||||
in: "query"
|
||||
description: "Stack deployment method. Possible values: string or repository."
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Stack details. Used when"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/StackCreateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/StackCreateResponse"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
404:
|
||||
description: "Endpoint not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Endpoint not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoints/{endpointId}/stacks/{id}:
|
||||
get:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "Inspect a stack"
|
||||
description: |
|
||||
Retrieve details about a stack.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Stack identifier"
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Stack"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Stack not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Stack not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
put:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "Update a stack"
|
||||
description: |
|
||||
Update a stack.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Stack identifier"
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Stack details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/StackUpdateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
404:
|
||||
description: "Stack not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Stack not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
delete:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "Remove a stack"
|
||||
description: |
|
||||
Remove a stack.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackDelete"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Stack identifier"
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Stack not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Stack not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoints/{endpointId}/stacks/{id}/stackfile:
|
||||
get:
|
||||
tags:
|
||||
- "stacks"
|
||||
summary: "Retrieve the content of the Stack file for the specified stack"
|
||||
description: |
|
||||
Get Stack file content.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackFileInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "endpointId"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Stack identifier"
|
||||
required: true
|
||||
type: "string"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/StackFileInspectResponse"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Stack not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Stack not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/registries:
|
||||
get:
|
||||
tags:
|
||||
|
@ -536,7 +817,7 @@ paths:
|
|||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Endpoint not found"
|
||||
err: "Registry not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
|
@ -593,13 +874,6 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
503:
|
||||
description: "Endpoint management disabled"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Endpoint management is disabled"
|
||||
delete:
|
||||
tags:
|
||||
- "registries"
|
||||
|
@ -1869,7 +2143,7 @@ definitions:
|
|||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.15.5"
|
||||
example: "1.16.0"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
|
@ -1883,7 +2157,7 @@ definitions:
|
|||
DisplayDonationHeader:
|
||||
type: "boolean"
|
||||
example: true
|
||||
description: "Whether to display or not the donation message in the header."\
|
||||
description: "Whether to display or not the donation message in the header."
|
||||
DisplayExternalContributors:
|
||||
type: "boolean"
|
||||
example: false
|
||||
|
@ -2612,3 +2886,104 @@ definitions:
|
|||
type: "string"
|
||||
example: "nginx:latest"
|
||||
description: "The Docker image associated to the template"
|
||||
StackCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Name"
|
||||
- "SwarmID"
|
||||
properties:
|
||||
Name:
|
||||
type: "string"
|
||||
example: "myStack"
|
||||
description: "Name of the stack"
|
||||
SwarmID:
|
||||
type: "string"
|
||||
example: "jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Cluster identifier of the Swarm cluster"
|
||||
StackFileContent:
|
||||
type: "string"
|
||||
example: "version: 3\n services:\n web:\n image:nginx"
|
||||
description: "Content of the Stack file. Required when using the 'string' deployment method."
|
||||
GitRepository:
|
||||
type: "string"
|
||||
example: "https://github.com/openfaas/faas"
|
||||
description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method."
|
||||
PathInRepository:
|
||||
type: "string"
|
||||
example: "docker-compose.yml"
|
||||
description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method."
|
||||
Env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during stack deployment"
|
||||
items:
|
||||
$ref: "#/definitions/Stack_Env"
|
||||
Stack_Env:
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
example: "MYSQL_ROOT_PASSWORD"
|
||||
value:
|
||||
type: "string"
|
||||
example: "password"
|
||||
StackCreateResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
Id:
|
||||
type: "string"
|
||||
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Id of the stack"
|
||||
StackListResponse:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Stack"
|
||||
Stack:
|
||||
type: "object"
|
||||
properties:
|
||||
Id:
|
||||
type: "string"
|
||||
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Stack identifier"
|
||||
Name:
|
||||
type: "string"
|
||||
example: "myStack"
|
||||
description: "Stack name"
|
||||
EntryPoint:
|
||||
type: "string"
|
||||
example: "docker-compose.yml"
|
||||
description: "Path to the Stack file"
|
||||
SwarmID:
|
||||
type: "string"
|
||||
example: "jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Cluster identifier of the Swarm cluster where the stack is deployed"
|
||||
ProjectPath:
|
||||
type: "string"
|
||||
example: "/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w"
|
||||
description: "Path on disk to the repository hosting the Stack file"
|
||||
Env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during stack deployment"
|
||||
items:
|
||||
$ref: "#/definitions/Stack_Env"
|
||||
StackUpdateRequest:
|
||||
type: "object"
|
||||
properties:
|
||||
StackFileContent:
|
||||
type: "string"
|
||||
example: "version: 3\n services:\n web:\n image:nginx"
|
||||
description: "New content of the Stack file."
|
||||
Env:
|
||||
type: "array"
|
||||
description: "A list of environment variables used during stack deployment"
|
||||
items:
|
||||
$ref: "#/definitions/Stack_Env"
|
||||
Prune:
|
||||
type: "boolean"
|
||||
example: false
|
||||
description: "Prune services that are no longer referenced"
|
||||
StackFileInspectResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
StackFileContent:
|
||||
type: "string"
|
||||
example: "version: 3\n services:\n web:\n image:nginx"
|
||||
description: "Content of the Stack file."
|
||||
|
|
|
@ -5,6 +5,7 @@ angular.module('portainer', [
|
|||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'ngMessages',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-jwt',
|
||||
|
@ -40,6 +41,7 @@ angular.module('portainer', [
|
|||
'endpointAccess',
|
||||
'endpoints',
|
||||
'events',
|
||||
'extension.storidge',
|
||||
'image',
|
||||
'images',
|
||||
'initAdmin',
|
||||
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer')
|
|||
StateManager.initialize()
|
||||
.then(function success(state) {
|
||||
if (state.application.authentication) {
|
||||
initAuthentication(authManager, Authentication, $rootScope);
|
||||
initAuthentication(authManager, Authentication, $rootScope, $state);
|
||||
}
|
||||
if (state.application.analytics) {
|
||||
initAnalytics(Analytics, $rootScope);
|
||||
|
@ -30,7 +30,7 @@ angular.module('portainer')
|
|||
}]);
|
||||
|
||||
|
||||
function initAuthentication(authManager, Authentication, $rootScope) {
|
||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
Authentication.init();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('containerLogs', [])
|
||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container',
|
||||
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container) {
|
||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications',
|
||||
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayTimestampsOut = false;
|
||||
$scope.state.displayTimestampsErr = false;
|
||||
|
|
|
@ -11,6 +11,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
NetworkContainer: '',
|
||||
Labels: [],
|
||||
ExtraHosts: [],
|
||||
MacAddress: '',
|
||||
IPv4: '',
|
||||
IPv6: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
|
@ -34,6 +35,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
Image: '',
|
||||
Env: [],
|
||||
Cmd: '',
|
||||
MacAddress: '',
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
|
@ -193,6 +195,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
config.Hostname = '';
|
||||
}
|
||||
config.HostConfig.NetworkMode = networkMode;
|
||||
config.MacAddress = $scope.formValues.MacAddress;
|
||||
|
||||
config.NetworkingConfig.EndpointsConfig[networkMode] = {
|
||||
IPAMConfig: {
|
||||
|
@ -387,6 +390,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
}
|
||||
$scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode];
|
||||
// Mac Address
|
||||
$scope.formValues.MacAddress = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].MacAddress;
|
||||
// ExtraHosts
|
||||
for (var h in $scope.config.HostConfig.ExtraHosts) {
|
||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.ExtraHosts, h)) {
|
||||
|
@ -396,7 +401,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
}
|
||||
|
||||
function loadFromContainerEnvrionmentVariables(d) {
|
||||
function loadFromContainerEnvironmentVariables(d) {
|
||||
var envArr = [];
|
||||
for (var e in $scope.config.Env) {
|
||||
if ({}.hasOwnProperty.call($scope.config.Env, e)) {
|
||||
|
@ -478,7 +483,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
loadFromContainerPortBindings(d);
|
||||
loadFromContainerVolumes(d);
|
||||
loadFromContainerNetworkConfig(d);
|
||||
loadFromContainerEnvrionmentVariables(d);
|
||||
loadFromContainerEnvironmentVariables(d);
|
||||
loadFromContainerLabels(d);
|
||||
loadFromContainerConsole(d);
|
||||
loadFromContainerDevices(d);
|
||||
|
|
|
@ -332,6 +332,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !domainname -->
|
||||
<!-- mac-address-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_macaddress" class="col-sm-2 col-lg-1 control-label text-left">Mac Address</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.MacAddress" id="container_macaddress" placeholder="e.g. 12-34-56-78-9a-bc">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !mac-address-input -->
|
||||
<!-- ipv4-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_ipv4" class="col-sm-2 col-lg-1 control-label text-left">IPv4 Address</label>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createService', [])
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService',
|
||||
function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService, SettingsService) {
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService',
|
||||
function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
|
@ -20,6 +20,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
Volumes: [],
|
||||
Network: '',
|
||||
ExtraNetworks: [],
|
||||
HostsEntries: [],
|
||||
Ports: [],
|
||||
Parallelism: 1,
|
||||
PlacementConstraints: [],
|
||||
|
@ -39,7 +40,9 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
RestartCondition: 'any',
|
||||
RestartDelay: '5s',
|
||||
RestartMaxAttempts: 0,
|
||||
RestartWindow: '0s'
|
||||
RestartWindow: '0s',
|
||||
LogDriverName: '',
|
||||
LogDriverOpts: []
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -69,6 +72,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
$scope.formValues.ExtraNetworks.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addHostsEntry = function() {
|
||||
$scope.formValues.HostsEntries.push({});
|
||||
};
|
||||
|
||||
$scope.removeHostsEntry = function(index) {
|
||||
$scope.formValues.HostsEntries.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addVolume = function() {
|
||||
$scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' });
|
||||
};
|
||||
|
@ -133,6 +144,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
$scope.formValues.ContainerLabels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function(value) {
|
||||
$scope.formValues.LogDriverOpts.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLogDriverOpt = function(index) {
|
||||
$scope.formValues.LogDriverOpts.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
|
@ -244,6 +263,22 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
config.Networks = _.uniqWith(networks, _.isEqual);
|
||||
}
|
||||
|
||||
function prepareHostsEntries(config, input) {
|
||||
var hostsEntries = [];
|
||||
if (input.HostsEntries) {
|
||||
input.HostsEntries.forEach(function (host_ip) {
|
||||
if (host_ip.value && host_ip.value.indexOf(':') && host_ip.value.split(':').length === 2) {
|
||||
var keyVal = host_ip.value.split(':');
|
||||
// Hosts file format, IP_address canonical_hostname
|
||||
hostsEntries.push(keyVal[1] + ' ' + keyVal[0]);
|
||||
}
|
||||
});
|
||||
if (hostsEntries.length > 0) {
|
||||
config.TaskTemplate.ContainerSpec.Hosts = hostsEntries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareUpdateConfig(config, input) {
|
||||
config.UpdateConfig = {
|
||||
Parallelism: input.Parallelism || 0,
|
||||
|
@ -330,6 +365,23 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
}
|
||||
}
|
||||
|
||||
function prepareLogDriverConfig(config, input) {
|
||||
var logOpts = {};
|
||||
if (input.LogDriverName) {
|
||||
config.TaskTemplate.LogDriver = { Name: input.LogDriverName };
|
||||
if (input.LogDriverName !== 'none') {
|
||||
input.LogDriverOpts.forEach(function (opt) {
|
||||
if (opt.name) {
|
||||
logOpts[opt.name] = opt.value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
||||
config.TaskTemplate.LogDriver.Options = logOpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var input = $scope.formValues;
|
||||
var config = {
|
||||
|
@ -355,6 +407,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
prepareLabelsConfig(config, input);
|
||||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
prepareHostsEntries(config, input);
|
||||
prepareUpdateConfig(config, input);
|
||||
prepareConfigConfig(config, input);
|
||||
prepareSecretConfig(config, input);
|
||||
|
@ -362,6 +415,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
prepareResourcesCpuConfig(config, input);
|
||||
prepareResourcesMemoryConfig(config, input);
|
||||
prepareRestartPolicy(config, input);
|
||||
prepareLogDriverConfig(config, input);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
@ -448,17 +502,17 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||
nodes: NodeService.nodes(),
|
||||
settings: SettingsService.publicSettings()
|
||||
settings: SettingsService.publicSettings(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25)
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.availableNetworks = data.networks;
|
||||
$scope.availableSecrets = data.secrets;
|
||||
$scope.availableConfigs = data.configs;
|
||||
var nodes = data.nodes;
|
||||
initSlidersMaxValuesBasedOnNodeData(nodes);
|
||||
var settings = data.settings;
|
||||
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
|
||||
$scope.availableConfigs = data.configs;
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
initSlidersMaxValuesBasedOnNodeData(data.nodes);
|
||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
})
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-pills nav-justified">
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command & Logging</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
|
@ -140,6 +140,9 @@
|
|||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Command
|
||||
</div>
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
|
||||
|
@ -195,6 +198,59 @@
|
|||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Logging
|
||||
</div>
|
||||
<!-- logging-driver -->
|
||||
<div class="form-group">
|
||||
<label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="form-control" ng-model="formValues.LogDriverName" id="log-driver">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Logging driver for service that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found <a href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers" target="_blank">in the Docker documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !logging-driver -->
|
||||
<!-- logging-opts -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">
|
||||
Options
|
||||
<portainer-tooltip position="top" message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="!formValues.LogDriverName || formValues.LogDriverName === 'none' || addLogDriverOpt(formValues.LogDriverName)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option
|
||||
</span>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="opt in formValues.LogDriverOpts" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">option</span>
|
||||
<input type="text" class="form-control" ng-model="opt.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="opt.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
</div>
|
||||
<!-- !logging-opts -->
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
|
@ -308,6 +364,29 @@
|
|||
<!-- !network-input-list -->
|
||||
</div>
|
||||
<!-- !extra-networks -->
|
||||
<!-- extra-hosts-variables -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Hosts file entries</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addHostsEntry()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
|
||||
</span>
|
||||
</div>
|
||||
<!-- hosts-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.HostsEntries" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. host:IP">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeHostsEntry($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !hosts-input-list -->
|
||||
</div>
|
||||
<!-- !extra-hosts-variables -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('createVolume', [])
|
||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator',
|
||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) {
|
||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', 'ExtensionManager',
|
||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, ExtensionManager) {
|
||||
|
||||
$scope.formValues = {
|
||||
Driver: 'local',
|
||||
|
@ -40,6 +40,12 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
|||
var name = $scope.formValues.Name;
|
||||
var driver = $scope.formValues.Driver;
|
||||
var driverOptions = $scope.formValues.DriverOptions;
|
||||
var storidgeProfile = $scope.formValues.StoridgeProfile;
|
||||
|
||||
if (driver === 'cio:latest' && storidgeProfile) {
|
||||
driverOptions.push({ name: 'profile', value: storidgeProfile.Name });
|
||||
}
|
||||
|
||||
var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions);
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
|
@ -82,5 +88,11 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
|||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
ExtensionManager.init()
|
||||
.then(function success(data) {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize extensions');
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -62,6 +62,14 @@
|
|||
<!-- !driver-options-input-list -->
|
||||
</div>
|
||||
<!-- !driver-options -->
|
||||
<!-- storidge -->
|
||||
<div ng-if="formValues.Driver === 'cio:latest'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storidge
|
||||
</div>
|
||||
<storidge-profile-selector storidge-profile="formValues.StoridgeProfile"></storidge-profile-selector>
|
||||
</div>
|
||||
<!-- storidge -->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<div>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Hosts file entries">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addHostsEntry(service)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add host entry
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!service.Hosts || service.Hosts.length === 0">
|
||||
<p>The Hosts file has no extra entries.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="service.Hosts.length > 0" classes="no-padding">
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="entry in service.Hosts">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="entry.hostname" placeholder="e.g. example.com" ng-change="updateHostsEntry(service, entry)" ng-disabled="isUpdating">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" ng-model="entry.ip" placeholder="e.g. 10.0.1.1" ng-change="updateHostsEntry(service, entry)" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeHostsEntry(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" 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(service, ['Hosts'])" ng-click="updateService(service)">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(service, ['Hosts'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,65 @@
|
|||
<div id="service-logging-driver">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Logging driver">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Driver:
|
||||
<select class="form-control" ng-model="service.LogDriverName" ng-change="updateLogDriverName(service)" ng-disabled="isUpdating">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option
|
||||
</a>
|
||||
</div>
|
||||
<table class="table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="option in service.LogDriverOpts">
|
||||
<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="option.key" ng-disabled="option.added || isUpdating" placeholder="e.g. FOO">
|
||||
</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="option.value" ng-change="updateLogDriverOpt(service, option)" placeholder="e.g. bar" ng-disabled="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.LogDriverOpts.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No options associated to this logging driver.</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(service, ['LogDriverName', 'LogDriverOpts'])" ng-click="updateService(service)">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(service, ['LogDriverName', 'LogDriverOpts'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -38,7 +38,6 @@
|
|||
<td>ID</td>
|
||||
<td>
|
||||
{{ service.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true" ng-disabled="isUpdating"></i>Delete this service</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.CreatedAt">
|
||||
|
@ -72,11 +71,17 @@
|
|||
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating">
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn" type="button" ui-sref="servicelogs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
</div>
|
||||
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="servicelogs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Service logs</a>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
||||
<span ng-show="state.updateInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.deletionInProgress || isUpdating" ng-click="removeService()" button-spinner="state.deletionInProgress">
|
||||
<span ng-hide="state.deletionInProgress"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete the service</span>
|
||||
<span ng-show="state.deletionInProgress">Deletion in progress...</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -116,6 +121,7 @@
|
|||
<li ng-if="applicationState.endpoint.apiVersion >= 1.30"><a href ng-click="goToItem('service-placement-preferences')">Placement preferences</a></li>
|
||||
<li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li>
|
||||
<li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li>
|
||||
<li><a href ng-click="goToItem('service-logging')">Logging</a></li>
|
||||
<li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
|
||||
<li><a href ng-click="goToItem('service-configs')">Configs</a></li>
|
||||
<li ng-if="applicationState.endpoint.apiVersion >= 1.25"><a href ng-click="goToItem('service-secrets')">Secrets</a></li>
|
||||
|
@ -152,6 +158,7 @@
|
|||
<h3 id="service-network-specs">Networks & ports</h3>
|
||||
<div id="service-networks" class="padding-top" ng-include="'app/components/service/includes/networks.html'"></div>
|
||||
<div id="service-published-ports" class="padding-top" ng-include="'app/components/service/includes/ports.html'"></div>
|
||||
<div id="service-hosts-entries" class="padding-top" ng-include="'app/components/service/includes/hosts.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -164,6 +171,7 @@
|
|||
<div id="service-placement-preferences" ng-if="applicationState.endpoint.apiVersion >= 1.30" class="padding-top" ng-include="'app/components/service/includes/placementPreferences.html'"></div>
|
||||
<div id="service-restart-policy" class="padding-top" ng-include="'app/components/service/includes/restart.html'"></div>
|
||||
<div id="service-update-config" class="padding-top" ng-include="'app/components/service/includes/updateconfig.html'"></div>
|
||||
<div id="service-logging" class="padding-top" ng-include="'app/components/service/includes/logging.html'"></div>
|
||||
<div id="service-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div>
|
||||
<div id="service-configs" class="padding-top" ng-include="'app/components/service/includes/configs.html'"></div>
|
||||
<div id="service-secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" class="padding-top" ng-include="'app/components/service/includes/secrets.html'"></div>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
angular.module('service', [])
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService) {
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService', 'PluginService',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService, PluginService) {
|
||||
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
deletionInProgress: false
|
||||
};
|
||||
|
||||
$scope.state = {};
|
||||
$scope.tasks = [];
|
||||
$scope.availableImages = [];
|
||||
|
||||
|
@ -168,6 +172,41 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
}
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function addLogDriverOpt(service) {
|
||||
service.LogDriverOpts.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
};
|
||||
$scope.removeLogDriverOpt = function removeLogDriverOpt(service, index) {
|
||||
var removedElement = service.LogDriverOpts.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
}
|
||||
};
|
||||
$scope.updateLogDriverOpt = function updateLogDriverOpt(service, variable) {
|
||||
if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
|
||||
updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts);
|
||||
}
|
||||
};
|
||||
$scope.updateLogDriverName = function updateLogDriverName(service) {
|
||||
updateServiceArray(service, 'LogDriverName', service.LogDriverName);
|
||||
};
|
||||
|
||||
$scope.addHostsEntry = function (service) {
|
||||
if (!service.Hosts) {
|
||||
service.Hosts = [];
|
||||
}
|
||||
service.Hosts.push({ hostname: '', ip: '' });
|
||||
};
|
||||
$scope.removeHostsEntry = function(service, index) {
|
||||
var removedElement = service.Hosts.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'Hosts', service.Hosts);
|
||||
}
|
||||
};
|
||||
$scope.updateHostsEntry = function(service, entry) {
|
||||
updateServiceArray(service, 'Hosts', service.Hosts);
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function cancelChanges(service, keys) {
|
||||
if (keys) { // clean out the keys only from the list of modified keys
|
||||
keys.forEach(function(key) {
|
||||
|
@ -203,6 +242,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
config.TaskTemplate.ContainerSpec.Image = service.Image;
|
||||
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
|
||||
config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
|
||||
config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
|
||||
|
||||
if (service.Mode === 'replicated') {
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
|
@ -244,6 +284,17 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
Window: ServiceHelper.translateHumanDurationToNanos(service.RestartWindow) || 0
|
||||
};
|
||||
|
||||
config.TaskTemplate.LogDriver = null;
|
||||
if (service.LogDriverName) {
|
||||
config.TaskTemplate.LogDriver = { Name: service.LogDriverName };
|
||||
if (service.LogDriverName !== 'none') {
|
||||
var logOpts = ServiceHelper.translateKeyValueToLogDriverOpts(service.LogDriverOpts);
|
||||
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
||||
config.TaskTemplate.LogDriver.Options = logOpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (service.Ports) {
|
||||
service.Ports.forEach(function (binding) {
|
||||
if (binding.PublishedPort === null || binding.PublishedPort === '') {
|
||||
|
@ -253,7 +304,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
}
|
||||
|
||||
config.EndpointSpec = {
|
||||
Mode: config.EndpointSpec.Mode || 'vip',
|
||||
Mode: (config.EndpointSpec && config.EndpointSpec.Mode) || 'vip',
|
||||
Ports: service.Ports
|
||||
};
|
||||
|
||||
|
@ -281,6 +332,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
};
|
||||
|
||||
function removeService() {
|
||||
$scope.state.deletionInProgress = true;
|
||||
ServiceService.remove($scope.service)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully deleted');
|
||||
|
@ -288,6 +340,39 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove service');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.deletionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.forceUpdateService = function(service) {
|
||||
ModalService.confirmServiceForceUpdate(
|
||||
'Do you want to force update this service? All the tasks associated to the selected service(s) will be recreated.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
forceUpdateService(service);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function forceUpdateService(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
// As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
|
||||
// value or an increment of the counter value to force an update.
|
||||
config.TaskTemplate.ForceUpdate++;
|
||||
$scope.state.updateInProgress = true;
|
||||
ServiceService.update(service, config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully updated', service.Name);
|
||||
$scope.cancelChanges({});
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to force update service', service.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.updateInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -295,11 +380,13 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
|
||||
service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : [];
|
||||
service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
|
||||
service.LogDriverOpts = ServiceHelper.translateLogDriverOptsToKeyValue(service.LogDriverOpts);
|
||||
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
|
||||
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
|
||||
service.ServiceMounts = angular.copy(service.Mounts);
|
||||
service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints);
|
||||
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
|
||||
service.Hosts = ServiceHelper.translateHostsEntriesToHostnameIP(service.Hosts);
|
||||
}
|
||||
|
||||
function transformResources(service) {
|
||||
|
@ -317,6 +404,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
|
||||
function initView() {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
ServiceService.service($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var service = data;
|
||||
|
@ -336,7 +424,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
nodes: NodeService.nodes(),
|
||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||
availableImages: ImageService.images()
|
||||
availableImages: ImageService.images(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25)
|
||||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
|
@ -345,6 +434,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
$scope.configs = data.configs;
|
||||
$scope.secrets = data.secrets;
|
||||
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
|
||||
// Set max cpu value
|
||||
var maxCpus = 0;
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
show-ownership-column="applicationState.application.authentication"
|
||||
remove-action="removeAction"
|
||||
scale-action="scaleAction"
|
||||
force-update-action="forceUpdateAction"
|
||||
swarm-manager-ip="swarmManagerIP"
|
||||
show-force-update-button="applicationState.endpoint.apiVersion >= 1.25"
|
||||
></services-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,39 @@ function ($q, $scope, $state, Service, ServiceService, ServiceHelper, Notificati
|
|||
});
|
||||
};
|
||||
|
||||
$scope.forceUpdateAction = function(selectedItems) {
|
||||
ModalService.confirmServiceForceUpdate(
|
||||
'Do you want to force update of selected service(s)? All the tasks associated to the selected service(s) will be recreated.',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
forceUpdateServices(selectedItems);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function forceUpdateServices(services) {
|
||||
var actionCount = services.length;
|
||||
angular.forEach(services, function (service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
// As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
|
||||
// value or an increment of the counter value to force an update.
|
||||
config.TaskTemplate.ForceUpdate++;
|
||||
ServiceService.update(service, config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Service successfully updated', service.Name);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to force update service', service.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.removeAction = function(selectedItems) {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.',
|
||||
|
|
|
@ -57,6 +57,18 @@
|
|||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
|
||||
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="applicationState.endpoint.extensions.length > 0 && isAdmin && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<span>Extensions</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<a ui-sref="storidge.cluster" ui-sref-active="active">Storidge <span class="menu-icon fa fa-bolt fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.create' || $state.current.name === 'storidge.profiles.edit')">
|
||||
<a ui-sref="storidge.monitor" ui-sref-active="active">Monitor</a>
|
||||
</div>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.create' || $state.current.name === 'storidge.profiles.edit')">
|
||||
<a ui-sref="storidge.profiles" ui-sref-active="active">Profiles</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||
<span>Portainer settings</span>
|
||||
</li>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('sidebar', [])
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService',
|
||||
function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService) {
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||
function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||
|
||||
$scope.uiVersion = StateManager.getState().application.version;
|
||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||
|
@ -12,8 +12,10 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
|||
var activeEndpointPublicURL = EndpointProvider.endpointPublicURL();
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
ExtensionManager.reset();
|
||||
$state.go('dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -90,6 +90,22 @@
|
|||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
<!-- options -->
|
||||
<div class="col-sm-12 form-section-title" ng-if="applicationState.endpoint.apiVersion >= 1.27">
|
||||
Options
|
||||
</div>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.27">
|
||||
<div class="col-sm-12">
|
||||
<label for="prune" class="control-label text-left">
|
||||
Prune services
|
||||
<portainer-tooltip position="bottom" message="Prune services that are no longer referenced."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="prune" type="checkbox" ng-model="formValues.Prune"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !options -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
angular.module('stack', [])
|
||||
.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', 'FormHelper', 'EndpointProvider',
|
||||
function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications, FormHelper, EndpointProvider) {
|
||||
.controller('StackController', ['$q', '$scope', '$state', '$transition$', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', 'FormHelper', 'EndpointProvider',
|
||||
function ($q, $scope, $state, $transition$, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications, FormHelper, EndpointProvider) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
publicURL: EndpointProvider.endpointPublicURL()
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Prune: false
|
||||
};
|
||||
|
||||
$scope.deployStack = function () {
|
||||
// The codemirror editor does not work with ng-model so we need to retrieve
|
||||
// the value directly from the editor.
|
||||
var stackFile = $scope.editor.getValue();
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.stack.Env);
|
||||
var prune = $scope.formValues.Prune;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StackService.updateStack($scope.stack.Id, stackFile, env)
|
||||
StackService.updateStack($scope.stack.Id, stackFile, env, prune)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Stack successfully deployed');
|
||||
$state.reload();
|
||||
|
@ -36,7 +41,8 @@ function ($q, $scope, $state, $stateParams, $document, StackService, NodeService
|
|||
};
|
||||
|
||||
function initView() {
|
||||
var stackId = $stateParams.id;
|
||||
var stackId = $transition$.params().id;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
StackService.stack(stackId)
|
||||
.then(function success(data) {
|
||||
|
|
|
@ -50,6 +50,25 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Refresh
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refreshRate" class="col-sm-1 margin-sm-top control-label text-left">
|
||||
Rate
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="display: none;"></i>
|
||||
</label>
|
||||
<div class="col-sm-2">
|
||||
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">60s</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
@ -75,6 +94,7 @@
|
|||
<div>{{ node.Role }}</div>
|
||||
<div>CPU: {{ node.CPUs / 1000000000 }}</div>
|
||||
<div>Memory: {{ node.Memory|humansize: 2 }}</div>
|
||||
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
||||
</div>
|
||||
<div class="tasks">
|
||||
<div class="task task_{{ task.Status.State | visualizerTask }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
||||
|
|
|
@ -1,12 +1,57 @@
|
|||
angular.module('swarmVisualizer', [])
|
||||
.controller('SwarmVisualizerController', ['$q', '$scope', '$document', 'NodeService', 'ServiceService', 'TaskService', 'Notifications',
|
||||
function ($q, $scope, $document, NodeService, ServiceService, TaskService, Notifications) {
|
||||
.controller('SwarmVisualizerController', ['$q', '$scope', '$document', '$interval', 'NodeService', 'ServiceService', 'TaskService', 'Notifications',
|
||||
function ($q, $scope, $document, $interval, NodeService, ServiceService, TaskService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
ShowInformationPanel: true,
|
||||
DisplayOnlyRunningTasks: false
|
||||
DisplayOnlyRunningTasks: false,
|
||||
refreshRate: '5'
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopRepeater();
|
||||
});
|
||||
|
||||
$scope.changeUpdateRepeater = function() {
|
||||
stopRepeater();
|
||||
setUpdateRepeater();
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(1500);
|
||||
};
|
||||
|
||||
function stopRepeater() {
|
||||
var repeater = $scope.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
$interval.cancel(repeater);
|
||||
repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setUpdateRepeater() {
|
||||
var refreshRate = $scope.state.refreshRate;
|
||||
$scope.repeater = $interval(function() {
|
||||
$q.all({
|
||||
nodes: NodeService.nodes(),
|
||||
services: ServiceService.services(),
|
||||
tasks: TaskService.tasks()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var nodes = data.nodes;
|
||||
$scope.nodes = nodes;
|
||||
var services = data.services;
|
||||
$scope.services = services;
|
||||
var tasks = data.tasks;
|
||||
$scope.tasks = tasks;
|
||||
prepareVisualizerData(nodes, services, tasks);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
|
||||
});
|
||||
}, refreshRate * 1000);
|
||||
}
|
||||
|
||||
|
||||
function assignServiceName(services, tasks) {
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
var service = services[i];
|
||||
|
@ -60,6 +105,7 @@ function ($q, $scope, $document, NodeService, ServiceService, TaskService, Notif
|
|||
var tasks = data.tasks;
|
||||
$scope.tasks = tasks;
|
||||
prepareVisualizerData(nodes, services, tasks);
|
||||
setUpdateRepeater();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize cluster visualizer');
|
||||
|
|
|
@ -288,6 +288,35 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !extra-host -->
|
||||
<!-- Label -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in state.selectedTemplate.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !Label -->
|
||||
</div>
|
||||
<!-- !advanced-options -->
|
||||
<!-- actions -->
|
||||
|
|
|
@ -45,6 +45,14 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
|||
$scope.state.selectedTemplate.Hosts.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLabel = function () {
|
||||
$scope.state.selectedTemplate.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.state.selectedTemplate.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
|
|
|
@ -118,6 +118,7 @@
|
|||
title="Users" title-icon="fa-user"
|
||||
dataset="users" table-key="users"
|
||||
order-by="Username" show-text-filter="true"
|
||||
authentication-method="AuthenticationMethod"
|
||||
remove-action="removeAction"
|
||||
></users-datatable>
|
||||
</div>
|
||||
|
|
|
@ -40,5 +40,6 @@ function ($q, $scope, $state, VolumeService, Notifications) {
|
|||
Notifications.error('Failure', err, 'Unable to retrieve volumes');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -11,4 +11,5 @@ angular.module('portainer')
|
|||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
|
||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10);
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('APPLICATION_CACHE_VALIDITY', 3600);
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
angular
|
||||
.module('portainer')
|
||||
angular.module('portainer')
|
||||
.directive('autoFocus', ['$timeout', function porAutoFocus($timeout) {
|
||||
var directive = {
|
||||
restrict: 'A',
|
||||
link: function($scope, $element) {
|
||||
link: function(scope, element) {
|
||||
$timeout(function() {
|
||||
$element[0].focus();
|
||||
element[0].focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
angular.module('portainer')
|
||||
.directive('onEnterKey', [function porOnEnterKey() {
|
||||
var directive = {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.bind('keydown keypress', function (e) {
|
||||
if ( e.which === 13 ) {
|
||||
e.preventDefault();
|
||||
scope.$apply(function () {
|
||||
scope.$eval(attrs.onEnterKey);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return directive;
|
||||
}]);
|
|
@ -202,7 +202,7 @@
|
|||
</div>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td><a ui-sref="image({ id: item.Image })">{{ item.Image | hideshasum }}</a></td>
|
||||
<td><a ui-sref="image({ id: item.Image })">{{ item.Image | trimshasum }}</a></td>
|
||||
<td>{{ item.IP ? item.IP : '-' }}</td>
|
||||
<td ng-if="$ctrl.swarmContainers">{{ item.hostIP }}</td>
|
||||
<td>
|
||||
|
|
|
@ -70,16 +70,22 @@ function (PaginationService, DatatableService) {
|
|||
|
||||
for (var i = 0; i < this.dataset.length; i++) {
|
||||
var item = this.dataset[i];
|
||||
if (item.Checked && item.Status === 'paused') {
|
||||
this.state.noPausedItemsSelected = false;
|
||||
} else if (item.Checked && (item.Status === 'stopped' || item.Status === 'created')) {
|
||||
this.state.noStoppedItemsSelected = false;
|
||||
} else if (item.Checked && item.Status === 'running') {
|
||||
this.state.noRunningItemsSelected = false;
|
||||
if (item.Checked) {
|
||||
this.updateSelectionStateBasedOnItemStatus(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.updateSelectionStateBasedOnItemStatus = function(item) {
|
||||
if (item.Status === 'paused') {
|
||||
this.state.noPausedItemsSelected = false;
|
||||
} else if (['stopped', 'created'].indexOf(item.Status) !== -1) {
|
||||
this.state.noStoppedItemsSelected = false;
|
||||
} else if (['running', 'healthy', 'unhealthy', 'starting'].indexOf(item.Status) !== -1) {
|
||||
this.state.noRunningItemsSelected = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
@ -138,7 +144,20 @@ function (PaginationService, DatatableService) {
|
|||
}
|
||||
availableStateFilters.push({ label: item.Status, display: true });
|
||||
}
|
||||
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
|
||||
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
|
||||
};
|
||||
|
||||
this.updateStoredFilters = function(storedFilters) {
|
||||
var datasetFilters = this.filters.state.values;
|
||||
|
||||
for (var i = 0; i < datasetFilters.length; i++) {
|
||||
var filter = datasetFilters[i];
|
||||
existingFilter = _.find(storedFilters, ['label', filter.label]);
|
||||
if (existingFilter && !existingFilter.display) {
|
||||
filter.display = existingFilter.display;
|
||||
this.filters.state.enabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
|
@ -153,7 +172,7 @@ function (PaginationService, DatatableService) {
|
|||
|
||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
if (storedFilters !== null) {
|
||||
this.filters = storedFilters;
|
||||
this.updateStoredFilters(storedFilters.state.values);
|
||||
}
|
||||
this.filters.state.open = false;
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="network({id: item.Id})">{{ item.Name | truncate:40 }}</a>
|
||||
<a ui-sref="network({id: item.Id})" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||
</td>
|
||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Scope }}</td>
|
||||
|
|
|
@ -12,10 +12,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button ng-if="$ctrl.showForceUpdateButton" type="button" class="btn btn-sm btn-primary"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.forceUpdateAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-refresh space-right" aria-hidden="true"></i>Update
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.service">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
|
||||
</button>
|
||||
|
@ -102,7 +108,7 @@
|
|||
</a>
|
||||
</span>
|
||||
<span ng-if="item.Mode === 'replicated' && item.Scale">
|
||||
<input class="input-sm" type="number" ng-model="item.Replicas" />
|
||||
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" />
|
||||
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
|
|
|
@ -12,6 +12,8 @@ angular.module('ui').component('servicesDatatable', {
|
|||
showOwnershipColumn: '<',
|
||||
removeAction: '<',
|
||||
scaleAction: '<',
|
||||
swarmManagerIp: '<'
|
||||
swarmManagerIp: '<',
|
||||
forceUpdateAction: '<',
|
||||
showForceUpdateButton: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ angular.module('ui').component('stackServicesDatatable', {
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
nodes: '<',
|
||||
publicURL: '<',
|
||||
publicUrl: '<',
|
||||
showTextFilter: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -70,8 +70,8 @@
|
|||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.Id === 1 || authenticationMethod !== 2">Internal</span>
|
||||
<span ng-if="item.Id !== 1 && authenticationMethod === 2">LDAP</span>
|
||||
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2">Internal</span>
|
||||
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
|
|
@ -9,6 +9,7 @@ angular.module('ui').component('usersDatatable', {
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
removeAction: '<'
|
||||
removeAction: '<',
|
||||
authenticationMethod: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
angular.module('extension.storidge', [])
|
||||
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
|
||||
var storidge = {
|
||||
name: 'storidge',
|
||||
abstract: true,
|
||||
url: '/storidge',
|
||||
views: {
|
||||
'content@': {
|
||||
template: '<div ui-view="content@"></div>'
|
||||
},
|
||||
'sidebar@': {
|
||||
template: '<div ui-view="sidebar@"></div>'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var profiles = {
|
||||
name: 'storidge.profiles',
|
||||
url: '/profiles',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/storidge/views/profiles/profiles.html',
|
||||
controller: 'StoridgeProfilesController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var profileCreation = {
|
||||
name: 'storidge.profiles.create',
|
||||
url: '/create',
|
||||
params: {
|
||||
profileName: ''
|
||||
},
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/storidge/views/profiles/create/createProfile.html',
|
||||
controller: 'CreateProfileController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var profileEdition = {
|
||||
name: 'storidge.profiles.edit',
|
||||
url: '/edit/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/storidge/views/profiles/edit/editProfile.html',
|
||||
controller: 'EditProfileController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var cluster = {
|
||||
name: 'storidge.cluster',
|
||||
url: '/cluster',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/storidge/views/cluster/cluster.html',
|
||||
controller: 'StoridgeClusterController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var monitor = {
|
||||
name: 'storidge.monitor',
|
||||
url: '/events',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/storidge/views/monitor/monitor.html',
|
||||
controller: 'StoridgeMonitorController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(storidge);
|
||||
$stateRegistryProvider.register(profiles);
|
||||
$stateRegistryProvider.register(profileCreation);
|
||||
$stateRegistryProvider.register(profileEdition);
|
||||
$stateRegistryProvider.register(cluster);
|
||||
$stateRegistryProvider.register(monitor);
|
||||
}]);
|
|
@ -0,0 +1,89 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Time')">
|
||||
Date
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Category')">
|
||||
Category
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Category' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Category' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Module')">
|
||||
Module
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Module' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Module' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Content')">
|
||||
Content
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Content' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Content' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>{{ item.Time }}</td>
|
||||
<td>{{ item.Category }}</td>
|
||||
<td>{{ item.Module }}</td>
|
||||
<td>{{ item.Content }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No events available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('extension.storidge').component('storidgeClusterEventsDatatable', {
|
||||
templateUrl: 'app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('IP')">
|
||||
IP Address
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Role')">
|
||||
Role
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
Status
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.IP }}</td>
|
||||
<td>{{ item.Role }}</td>
|
||||
<td>
|
||||
<i class="fa fa-heartbeat space-right green-icon"></i>
|
||||
{{ item.Status }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No nodes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('extension.storidge').component('storidgeNodesDatatable', {
|
||||
templateUrl: 'app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
<div class="form-group">
|
||||
<label for="storidge_profile" class="col-sm-2 col-lg-1 control-label text-left">Profile</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select id="storidge_profile" ng-model="$ctrl.storidgeProfile" ng-options="profile.Name for profile in $ctrl.profiles" class="form-control">
|
||||
<option selected disabled hidden value="">Select a profile</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('extension.storidge').component('storidgeProfileSelector', {
|
||||
templateUrl: 'app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html',
|
||||
controller: 'StoridgeProfileSelectorController',
|
||||
bindings: {
|
||||
'storidgeProfile': '='
|
||||
}
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeProfileSelectorController', ['StoridgeProfileService', 'Notifications',
|
||||
function (StoridgeProfileService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
function initComponent() {
|
||||
StoridgeProfileService.profiles()
|
||||
.then(function success(data) {
|
||||
ctrl.profiles = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve Storidge profiles');
|
||||
});
|
||||
}
|
||||
|
||||
initComponent();
|
||||
}]);
|
|
@ -0,0 +1,84 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="storidge.profiles.edit({id: item.Name})">{{ item.Name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td class="text-center text-muted">No profile available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('extension.storidge').component('storidgeProfilesDatatable', {
|
||||
templateUrl: 'app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
function StoridgeEventModel(data) {
|
||||
this.Time = data.time;
|
||||
this.Category = data.category;
|
||||
this.Module = data.module;
|
||||
this.Content = data.content;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
function StoridgeInfoModel(data) {
|
||||
this.Domain = data.domain;
|
||||
this.Nodes = data.nodes;
|
||||
this.Status = data.status;
|
||||
this.ProvisionedBandwidth = data.provisionedBandwidth;
|
||||
this.UsedBandwidth = data.usedBandwidth;
|
||||
this.FreeBandwidth = data.freeBandwidth;
|
||||
this.TotalBandwidth = data.totalBandwidth;
|
||||
this.ProvisionedIOPS = data.provisionedIOPS;
|
||||
this.UsedIOPS = data.usedIOPS;
|
||||
this.FreeIOPS = data.freeIOPS;
|
||||
this.TotalIOPS = data.totalIOPS;
|
||||
this.ProvisionedCapacity = data.provisionedCapacity;
|
||||
this.UsedCapacity = data.usedCapacity;
|
||||
this.FreeCapacity = data.freeCapacity;
|
||||
this.TotalCapacity = data.totalCapacity;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
function StoridgeNodeModel(name, data) {
|
||||
this.Name = name;
|
||||
this.IP = data.ip;
|
||||
this.Role = data.role;
|
||||
this.Status = data.status;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
function StoridgeProfileDefaultModel() {
|
||||
this.Directory = '/cio/';
|
||||
this.Capacity = 20;
|
||||
this.Redundancy = 2;
|
||||
this.Provisioning = 'thin';
|
||||
this.Type = 'ssd';
|
||||
this.MinIOPS = 100;
|
||||
this.MaxIOPS = 2000;
|
||||
this.MinBandwidth = 1;
|
||||
this.MaxBandwidth = 100;
|
||||
}
|
||||
|
||||
function StoridgeProfileListModel(data) {
|
||||
this.Name = data;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
||||
function StoridgeProfileModel(name, data) {
|
||||
this.Name = name;
|
||||
this.Directory = data.directory;
|
||||
this.Capacity = data.capacity;
|
||||
this.Provisioning = data.provision;
|
||||
this.Type = data.type;
|
||||
this.Redundancy = data.level;
|
||||
|
||||
if (data.iops) {
|
||||
this.MinIOPS = data.iops.min;
|
||||
this.MaxIOPS = data.iops.max;
|
||||
}
|
||||
|
||||
if (data.bandwidth) {
|
||||
this.MinBandwidth = data.bandwidth.min;
|
||||
this.MaxBandwidth = data.bandwidth.max;
|
||||
}
|
||||
}
|
||||
|
||||
function StoridgeCreateProfileRequest(model) {
|
||||
this.name = model.Name;
|
||||
this.capacity = model.Capacity;
|
||||
this.directory = model.Directory;
|
||||
this.provision = model.Provisioning;
|
||||
this.type = model.Type;
|
||||
this.level = model.Redundancy;
|
||||
if (model.MinIOPS && model.MaxIOPS) {
|
||||
this.iops = {
|
||||
min: model.MinIOPS,
|
||||
max: model.MaxIOPS
|
||||
};
|
||||
}
|
||||
|
||||
if (model.MinBandwidth && model.MaxBandwidth) {
|
||||
this.bandwidth = {
|
||||
min: model.MinBandwidth,
|
||||
max: model.MaxBandwidth
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeCluster', ['$http', 'StoridgeManager', function StoridgeClusterFactory($http, StoridgeManager) {
|
||||
'use strict';
|
||||
|
||||
var service = {};
|
||||
|
||||
service.queryEvents = function() {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/events',
|
||||
skipAuthorization: true,
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true
|
||||
});
|
||||
};
|
||||
|
||||
service.queryVersion = function() {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/version',
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.queryInfo = function() {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/info',
|
||||
skipAuthorization: true,
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true
|
||||
});
|
||||
};
|
||||
|
||||
service.reboot = function() {
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/cluster/reboot',
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,24 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeNodes', ['$http', 'StoridgeManager', function StoridgeNodesFactory($http, StoridgeManager) {
|
||||
'use strict';
|
||||
|
||||
var service = {};
|
||||
|
||||
service.query = function() {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/nodes',
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.inspect = function(id) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/nodes/' + id,
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,52 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeProfiles', ['$http', 'StoridgeManager', function StoridgeProfilesFactory($http, StoridgeManager) {
|
||||
'use strict';
|
||||
|
||||
var service = {};
|
||||
|
||||
service.create = function(payload) {
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles',
|
||||
data: payload,
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.update = function(id, payload) {
|
||||
return $http({
|
||||
method: 'PUT',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
||||
data: payload,
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.query = function() {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles',
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.inspect = function(id) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
service.delete = function(id) {
|
||||
return $http({
|
||||
method: 'DELETE',
|
||||
url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id,
|
||||
skipAuthorization: true
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,189 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeChartService', [function StoridgeChartService() {
|
||||
'use strict';
|
||||
|
||||
// Max. number of items to display on a chart
|
||||
var CHART_LIMIT = 600;
|
||||
|
||||
var service = {};
|
||||
|
||||
service.CreateCapacityChart = function(context) {
|
||||
return new Chart(context, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgba(171, 213, 255, 0.7)',
|
||||
'rgba(229, 57, 53, 0.7)'
|
||||
]
|
||||
}
|
||||
],
|
||||
labels: []
|
||||
},
|
||||
options: {
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
var dataset = data.datasets[tooltipItem.datasetIndex];
|
||||
var label = data.labels[tooltipItem.index];
|
||||
var value = dataset.data[tooltipItem.index];
|
||||
return label + ': ' + filesize(value, {base: 10, round: 1});
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsiveAnimationDuration: 0,
|
||||
responsive: true,
|
||||
hover: {
|
||||
animationDuration: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
service.CreateIOPSChart = function(context) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'IOPS',
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsiveAnimationDuration: 0,
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
position: 'nearest'
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 0
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
service.CreateBandwidthChart = function(context) {
|
||||
return new Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Bandwidth',
|
||||
data: [],
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||
borderColor: 'rgba(151,187,205,0.6)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: 'rgba(151,187,205,1)',
|
||||
pointRadius: 2,
|
||||
borderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
responsiveAnimationDuration: 0,
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
position: 'nearest',
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
|
||||
return bytePerSecBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
|
||||
}
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 0
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: bytePerSecBasedAxisLabel
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
service.UpdateChart = function(label, value, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(value);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
|
||||
service.UpdatePieChart = function(label, value, chart) {
|
||||
var idx = chart.data.labels.indexOf(label);
|
||||
if (idx > -1) {
|
||||
chart.data.datasets[0].data[idx] = value;
|
||||
} else {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(value);
|
||||
}
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
|
||||
function bytePerSecBasedTooltipLabel(label, value) {
|
||||
var processedValue = 0;
|
||||
if (value > 5) {
|
||||
processedValue = filesize(value, {base: 10, round: 1});
|
||||
} else {
|
||||
processedValue = value.toFixed(1) + 'B';
|
||||
}
|
||||
return label + ': ' + processedValue + '/s';
|
||||
}
|
||||
|
||||
function bytePerSecBasedAxisLabel(value, index, values) {
|
||||
if (value > 5) {
|
||||
return filesize(value, {base: 10, round: 1});
|
||||
}
|
||||
return value.toFixed(1) + 'B/s';
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,58 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeClusterService', ['$q', 'StoridgeCluster', function StoridgeClusterServiceFactory($q, StoridgeCluster) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.reboot = function() {
|
||||
return StoridgeCluster.reboot();
|
||||
};
|
||||
|
||||
service.info = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeCluster.queryInfo()
|
||||
.then(function success(response) {
|
||||
var info = new StoridgeInfoModel(response.data);
|
||||
deferred.resolve(info);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge information', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.version = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeCluster.queryVersion()
|
||||
.then(function success(response) {
|
||||
var version = response.data.version;
|
||||
deferred.resolve(version);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge version', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.events = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeCluster.queryEvents()
|
||||
.then(function success(response) {
|
||||
var events = response.data.map(function(item) {
|
||||
return new StoridgeEventModel(item);
|
||||
});
|
||||
deferred.resolve(events);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge events', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,48 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeManager', ['$q', 'LocalStorage', 'SystemService', function StoridgeManagerFactory($q, LocalStorage, SystemService) {
|
||||
'use strict';
|
||||
var service = {
|
||||
API: ''
|
||||
};
|
||||
|
||||
service.init = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
var storedAPIURL = LocalStorage.getStoridgeAPIURL();
|
||||
if (storedAPIURL) {
|
||||
service.API = storedAPIURL;
|
||||
deferred.resolve();
|
||||
} else {
|
||||
SystemService.info()
|
||||
.then(function success(data) {
|
||||
var endpointAddress = LocalStorage.getEndpointPublicURL();
|
||||
var storidgeAPIURL = '';
|
||||
if (endpointAddress) {
|
||||
storidgeAPIURL = 'http://' + endpointAddress + ':8282';
|
||||
} else {
|
||||
var managerIP = data.Swarm.NodeAddr;
|
||||
storidgeAPIURL = 'http://' + managerIP + ':8282';
|
||||
}
|
||||
|
||||
service.API = storidgeAPIURL;
|
||||
LocalStorage.storeStoridgeAPIURL(storidgeAPIURL);
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge API URL', err: err });
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.reset = function() {
|
||||
LocalStorage.clearStoridgeAPIURL();
|
||||
};
|
||||
|
||||
service.StoridgeAPIURL = function() {
|
||||
return service.API;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,30 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeNodeService', ['$q', 'StoridgeNodes', function StoridgeNodeServiceFactory($q, StoridgeNodes) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.nodes = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeNodes.query()
|
||||
.then(function success(response) {
|
||||
var nodeData = response.data.nodes;
|
||||
var nodes = [];
|
||||
|
||||
for (var key in nodeData) {
|
||||
if (nodeData.hasOwnProperty(key)) {
|
||||
nodes.push(new StoridgeNodeModel(key, nodeData[key]));
|
||||
}
|
||||
}
|
||||
|
||||
deferred.resolve(nodes);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge profiles', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,53 @@
|
|||
angular.module('extension.storidge')
|
||||
.factory('StoridgeProfileService', ['$q', 'StoridgeProfiles', function StoridgeProfileServiceFactory($q, StoridgeProfiles) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.create = function(model) {
|
||||
var payload = new StoridgeCreateProfileRequest(model);
|
||||
return StoridgeProfiles.create(payload);
|
||||
};
|
||||
|
||||
service.update = function(model) {
|
||||
var payload = new StoridgeCreateProfileRequest(model);
|
||||
return StoridgeProfiles.update(model.Name, payload);
|
||||
};
|
||||
|
||||
service.delete = function(profileName) {
|
||||
return StoridgeProfiles.delete(profileName);
|
||||
};
|
||||
|
||||
service.profile = function(profileName) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeProfiles.inspect(profileName)
|
||||
.then(function success(response) {
|
||||
var profile = new StoridgeProfileModel(profileName, response.data);
|
||||
deferred.resolve(profile);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge profile details', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.profiles = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
StoridgeProfiles.query()
|
||||
.then(function success(response) {
|
||||
var profiles = response.data.profiles.map(function (item) {
|
||||
return new StoridgeProfileListModel(item);
|
||||
});
|
||||
deferred.resolve(profiles);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge profiles', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,145 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Storidge cluster">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.cluster" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-bolt" title="Cluster details"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Domain</td>
|
||||
<td>{{ clusterInfo.Domain }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><i class="fa fa-heartbeat space-right green-icon"></i> {{ clusterInfo.Status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ clusterVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="shutdownCluster()" ng-disabled="state.shutdownInProgress" button-spinner="state.shutdownInProgress">
|
||||
<span ng-hide="state.updateInProgress"><i class="fa fa-power-off space-right" aria-hidden="true"></i> Shutdown the cluster</span>
|
||||
<span ng-show="state.updateInProgress">Shutting down cluster...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="rebootCluster()" ng-disabled="state.rebootInProgress" button-spinner="state.shutdownInProgress">
|
||||
<span ng-hide="state.deleteInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i> Reboot the cluster</span>
|
||||
<span ng-show="state.deleteInProgress">Rebooting cluster...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<storidge-nodes-datatable
|
||||
title="Storage nodes" title-icon="fa-object-group"
|
||||
dataset="clusterNodes" table-key="storidge_nodes"
|
||||
order-by="Name" show-text-filter="true"
|
||||
></storidge-nodes-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="row" ng-if="clusterInfo">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Storage nodes">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('IP')">
|
||||
IP address
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Role')">
|
||||
Role
|
||||
<span ng-show="sortType == 'Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="node in (clusterNodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td>{{ node.Name }}</td>
|
||||
<td>{{ node.IP }}</td>
|
||||
<td>{{ node.Role }}</td>
|
||||
<td>
|
||||
<i class="fa fa-heartbeat space-right green-icon"></i>
|
||||
{{ node.Status }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!clusterNodes">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="clusterNodes.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No nodes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="clusterNodes" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div> -->
|
|
@ -0,0 +1,87 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'StoridgeManager', 'ModalService',
|
||||
function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, StoridgeManager, ModalService) {
|
||||
|
||||
$scope.state = {
|
||||
shutdownInProgress: false,
|
||||
rebootInProgress: false
|
||||
};
|
||||
|
||||
$scope.rebootCluster = function() {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'All the nodes in the cluster will reboot during the process. Do you want to reboot the Storidge cluster?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Reboot',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
rebootCluster();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.shutdownCluster = function() {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'All the nodes in the cluster will shutdown. Do you want to shutdown the Storidge cluster?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Shutdown',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
shutdownCluster();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function shutdownCluster() {
|
||||
Notifications.error('Not implemented', {}, 'Not implemented yet');
|
||||
$state.reload();
|
||||
}
|
||||
|
||||
function rebootCluster() {
|
||||
$scope.state.rebootInProgress = true;
|
||||
StoridgeClusterService.reboot()
|
||||
.then(function success(data) {
|
||||
Notifications.success('Cluster successfully rebooted');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to reboot cluster');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.rebootInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
info: StoridgeClusterService.info(),
|
||||
version: StoridgeClusterService.version(),
|
||||
nodes: StoridgeNodeService.nodes()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.clusterInfo = data.info;
|
||||
$scope.clusterVersion = data.version;
|
||||
$scope.clusterNodes = data.nodes;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
|
||||
});
|
||||
}
|
||||
|
||||
StoridgeManager.init()
|
||||
.then(function success() {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,201 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Storidge monitor">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.monitor" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.monitor">Cluster monitoring</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Cluster capacity"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="capacityChart" width="770" height="400"></canvas>
|
||||
</div>
|
||||
<div style="margin-top: 10px;" ng-if="info">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Capacity available</td>
|
||||
<td>{{ ((info.FreeCapacity * 100) / info.TotalCapacity).toFixed(1) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned capacity</td>
|
||||
<td>
|
||||
{{ info.ProvisionedCapacity | humansize }}
|
||||
<span ng-if="+(info.ProvisionedCapacity) >= +(info.TotalCapacity)">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total capacity</td>
|
||||
<td>{{ info.TotalCapacity | humansize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="IOPS usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="iopsChart" width="770" height="400"></canvas>
|
||||
</div>
|
||||
<div style="margin-top: 10px;" ng-if="info">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IOPS available</td>
|
||||
<td>{{ ((info.FreeIOPS * 100) / info.TotalIOPS).toFixed(1) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned IOPS</td>
|
||||
<td>
|
||||
{{ info.ProvisionedIOPS | number }}
|
||||
<span ng-if="+(info.ProvisionedIOPS) >= +(info.TotalIOPS)">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total IOPS</td>
|
||||
<td>{{ info.TotalIOPS | number }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Bandwith usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="bandwithChart" width="770" height="400"></canvas>
|
||||
</div>
|
||||
<div style="margin-top: 10px;" ng-if="info">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Bandwidth available</td>
|
||||
<td>{{ ((info.FreeBandwidth * 100) / info.TotalBandwidth).toFixed(1) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned bandwidth</td>
|
||||
<td>
|
||||
{{ info.ProvisionedBandwidth | humansize }}
|
||||
<span ng-if="+(info.ProvisionedBandwidth) >= +(info.TotalBandwidth)">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total bandwidth</td>
|
||||
<td>{{ info.TotalBandwidth | humansize }} /s</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<storidge-cluster-events-datatable
|
||||
title="Cluster events" title-icon="fa-history"
|
||||
dataset="events" table-key="storidge_cluster_events"
|
||||
order-by="Time" show-text-filter="true" reverse-order="true"
|
||||
></storidge-cluster-events-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-history" title="Cluster events">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="order('Time')">
|
||||
Date
|
||||
<span ng-show="sortType == 'Time' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Time' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Category')">
|
||||
Category
|
||||
<span ng-show="sortType == 'Category' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Category' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Module')">
|
||||
Module
|
||||
<span ng-show="sortType == 'Module' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Module' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Content')">
|
||||
Content
|
||||
<span ng-show="sortType == 'Content' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Content' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td>{{ event.Time }}</td>
|
||||
<td>{{ event.Category }}</td>
|
||||
<td>{{ event.Module }}</td>
|
||||
<td>{{ event.Content }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!events">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="events.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No events available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="events" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div> -->
|
|
@ -0,0 +1,108 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'StoridgeManager', 'ModalService',
|
||||
function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, StoridgeManager, ModalService) {
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopRepeater();
|
||||
});
|
||||
|
||||
function stopRepeater() {
|
||||
var repeater = $scope.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
$interval.cancel(repeater);
|
||||
repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateIOPSChart(info, chart) {
|
||||
var usedIOPS = info.UsedIOPS;
|
||||
var label = moment(new Date()).format('HH:mm:ss');
|
||||
|
||||
StoridgeChartService.UpdateChart(label, usedIOPS, chart);
|
||||
}
|
||||
|
||||
function updateBandwithChart(info, chart) {
|
||||
var usedBandwidth = info.UsedBandwidth;
|
||||
var label = moment(new Date()).format('HH:mm:ss');
|
||||
|
||||
StoridgeChartService.UpdateChart(label, usedBandwidth, chart);
|
||||
}
|
||||
|
||||
function updateCapacityChart(info, chart) {
|
||||
var usedCapacity = info.UsedCapacity;
|
||||
var freeCapacity = info.FreeCapacity;
|
||||
|
||||
StoridgeChartService.UpdatePieChart('Free', freeCapacity, chart);
|
||||
StoridgeChartService.UpdatePieChart('Used', usedCapacity, chart);
|
||||
}
|
||||
|
||||
function setUpdateRepeater(iopsChart, bandwidthChart, capacityChart) {
|
||||
var refreshRate = 5000;
|
||||
$scope.repeater = $interval(function() {
|
||||
$q.all({
|
||||
events: StoridgeClusterService.events(),
|
||||
info: StoridgeClusterService.info()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.events = data.events;
|
||||
var info = data.info;
|
||||
$scope.info = info;
|
||||
updateIOPSChart(info, iopsChart);
|
||||
updateBandwithChart(info, bandwidthChart);
|
||||
updateCapacityChart(info, capacityChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
|
||||
});
|
||||
}, refreshRate);
|
||||
}
|
||||
|
||||
function startViewUpdate(iopsChart, bandwidthChart, capacityChart) {
|
||||
$q.all({
|
||||
events: StoridgeClusterService.events(),
|
||||
info: StoridgeClusterService.info()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.events = data.events;
|
||||
var info = data.info;
|
||||
$scope.info = info;
|
||||
updateIOPSChart(info, iopsChart);
|
||||
updateBandwithChart(info, bandwidthChart);
|
||||
updateCapacityChart(info, capacityChart);
|
||||
setUpdateRepeater(iopsChart, bandwidthChart, capacityChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
|
||||
});
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
var iopsChartCtx = $('#iopsChart');
|
||||
var iopsChart = StoridgeChartService.CreateIOPSChart(iopsChartCtx);
|
||||
|
||||
var bandwidthChartCtx = $('#bandwithChart');
|
||||
var bandwidthChart = StoridgeChartService.CreateBandwidthChart(bandwidthChartCtx);
|
||||
|
||||
var capacityChartCtx = $('#capacityChart');
|
||||
var capacityChart = StoridgeChartService.CreateCapacityChart(capacityChartCtx);
|
||||
|
||||
startViewUpdate(iopsChart, bandwidthChart, capacityChart);
|
||||
}
|
||||
|
||||
function initView() {
|
||||
|
||||
$document.ready(function() {
|
||||
initCharts();
|
||||
});
|
||||
}
|
||||
|
||||
StoridgeManager.init()
|
||||
.then(function success() {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,200 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Create profile"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.profiles">Profiles</a> > Add profile
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="storidgeCreateProfileForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_name.$invalid }">
|
||||
<label for="profile_name" class="col-sm-2 col-lg-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="model.Name" name="profile_name" placeholder="e.g. myProfile" ng-change="updatedName()" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_name.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.profile_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Profile configuration
|
||||
</div>
|
||||
<!-- directory -->
|
||||
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_directory.$invalid }">
|
||||
<label for="profile_directory" class="col-sm-2 col-lg-1 control-label text-left">Directory</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="model.Directory" name="profile_directory" placeholder="e.g. /cio/myProfile" ng-change="updatedDirectory()" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_directory.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.profile_directory.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !directory -->
|
||||
<!-- capacity -->
|
||||
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_capacity.$invalid }">
|
||||
<label for="profile_capacity" class="col-sm-2 col-lg-1 control-label text-left">Capacity</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="model.Capacity" name="profile_capacity" ng-min="1" ng-max="64000" placeholder="2" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_capacity.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.profile_capacity.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for capacity: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for capacity: 64000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !capacity -->
|
||||
<!-- redundancy -->
|
||||
<div class="form-group">
|
||||
<label for="profile_redundancy" class="col-sm-2 col-lg-1 control-label text-left">Redundancy</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_redundancy" ng-model="model.Redundancy" ng-options="+(opt.value) as opt.label for opt in RedundancyOptions" class="form-control">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !redudancy -->
|
||||
<!-- provisioning -->
|
||||
<div class="form-group">
|
||||
<label for="profile_provisioning" class="col-sm-2 col-lg-1 control-label text-left">Provisioning</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_provisioning" ng-model="model.Provisioning" class="form-control">
|
||||
<option value="thin">Thin</option>
|
||||
<option value="thick">Thick</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !provisioning -->
|
||||
<!-- type -->
|
||||
<div class="form-group">
|
||||
<label for="profile_type" class="col-sm-2 col-lg-1 control-label text-left">Type</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_type" ng-model="model.Type" class="form-control">
|
||||
<option value="ssd">SSD</option>
|
||||
<option value="hdd">HDD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !type -->
|
||||
<!-- iops -->
|
||||
<div ng-if="!state.LimitBandwidth || state.NoLimit">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
IOPS
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="permissions" class="control-label text-left">
|
||||
Limit IOPS
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="state.LimitIOPS" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.LimitIOPS">
|
||||
<label for="min_iops" class="col-sm-1 control-label text-left">Min</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.min_iops.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="model.MinIOPS" name="min_iops" ng-min="30" ng-max="999999" placeholder="100" required>
|
||||
</div>
|
||||
<label for="max_iops" class="col-sm-1 control-label text-left">Max</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.max_iops.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="model.MaxIOPS" name="max_iops" ng-min="30" ng-max="999999" placeholder="2000" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.min_iops.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.min_iops.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min IOPS.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min IOPS: 30.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min IOPS: 999999.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.max_iops.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.max_iops.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max IOPS.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max IOPS: 30.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max IOPS: 999999.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !iops -->
|
||||
<!-- bandwidth -->
|
||||
<div ng-if="!state.LimitIOPS || state.NoLimit">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Bandwidth
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="permissions" class="control-label text-left">
|
||||
Limit bandwidth
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="state.LimitBandwidth" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.LimitBandwidth">
|
||||
<label for="min_bandwidth" class="col-sm-1 control-label text-left">Min</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.min_bandwidth.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="model.MinBandwidth" name="min_bandwidth" ng-min="1" ng-max="5000" placeholder="1" required>
|
||||
</div>
|
||||
<label for="max_bandwidth" class="col-sm-1 control-label text-left">Max</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.max_bandwidth.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="model.MaxBandwidth" name="max_bandwidth" ng-min="1" ng-max="5000" placeholder="100" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.min_bandwidth.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.min_bandwidth.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min bandwidth.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min bandwidth: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min bandwidth: 5000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeCreateProfileForm.max_bandwidth.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeCreateProfileForm.max_bandwidth.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max bandwidth.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max bandwidth: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max bandwidth: 5000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !bandwidth -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="create()" ng-disabled="state.actionInProgress || !storidgeCreateProfileForm.$valid" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Create the profile</span>
|
||||
<span ng-show="state.actionInProgress">Creating profile...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,72 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('CreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager',
|
||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager) {
|
||||
|
||||
$scope.state = {
|
||||
NoLimit: true,
|
||||
LimitIOPS: false,
|
||||
LimitBandwidth: false,
|
||||
ManualInputDirectory: false,
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.RedundancyOptions = [
|
||||
{ value: 2, label: '2-copy' },
|
||||
{ value: 3, label: '3-copy' }
|
||||
];
|
||||
|
||||
$scope.create = function () {
|
||||
var profile = $scope.model;
|
||||
|
||||
if (!$scope.state.LimitIOPS) {
|
||||
delete profile.MinIOPS;
|
||||
delete profile.MaxIOPS;
|
||||
}
|
||||
|
||||
if (!$scope.state.LimitBandwidth) {
|
||||
delete profile.MinBandwidth;
|
||||
delete profile.MaxBandwidth;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StoridgeProfileService.create(profile)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Profile successfully created');
|
||||
$state.go('storidge.profiles');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create profile');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updatedName = function() {
|
||||
if (!$scope.state.ManualInputDirectory) {
|
||||
var profile = $scope.model;
|
||||
profile.Directory = '/cio/' + profile.Name;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatedDirectory = function() {
|
||||
if (!$scope.state.ManualInputDirectory) {
|
||||
$scope.state.ManualInputDirectory = true;
|
||||
}
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var profile = new StoridgeProfileDefaultModel();
|
||||
profile.Name = $transition$.params().profileName;
|
||||
profile.Directory = '/cio/' + profile.Name;
|
||||
$scope.model = profile;
|
||||
}
|
||||
|
||||
StoridgeManager.init()
|
||||
.then(function success() {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,198 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Profile details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.profiles">Profiles</a> > {{ profile.Name }}
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="profile">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="storidgeUpdateProfileForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="profile_name" class="col-sm-2 col-lg-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="profile.Name" name="profile_name" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Profile configuration
|
||||
</div>
|
||||
<!-- directory -->
|
||||
<div class="form-group" ng-class="{ 'has-error': storidgeUpdateProfileForm.profile_directory.$invalid }">
|
||||
<label for="profile_directory" class="col-sm-2 col-lg-1 control-label text-left">Directory</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="profile.Directory" name="profile_directory" placeholder="e.g. /cio/myProfile" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.profile_directory.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.profile_directory.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !directory -->
|
||||
<!-- capacity -->
|
||||
<div class="form-group" ng-class="{ 'has-error': storidgeUpdateProfileForm.profile_capacity.$invalid }">
|
||||
<label for="profile_capacity" class="col-sm-2 col-lg-1 control-label text-left">Capacity</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="profile.Capacity" name="profile_capacity" ng-min="1" ng-max="64000" placeholder="2" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.profile_capacity.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.profile_capacity.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for capacity: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for capacity: 64000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !capacity -->
|
||||
<!-- redundancy -->
|
||||
<div class="form-group">
|
||||
<label for="profile_redundancy" class="col-sm-2 col-lg-1 control-label text-left">Redundancy</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_redundancy" ng-model="profile.Redundancy" ng-options="+(opt.value) as opt.label for opt in RedundancyOptions" class="form-control">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !redudancy -->
|
||||
<!-- provisioning -->
|
||||
<div class="form-group">
|
||||
<label for="profile_provisioning" class="col-sm-2 col-lg-1 control-label text-left">Provisioning</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_provisioning" ng-model="profile.Provisioning" class="form-control">
|
||||
<option value="thin">Thin</option>
|
||||
<option value="thick">Thick</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !provisioning -->
|
||||
<!-- type -->
|
||||
<div class="form-group">
|
||||
<label for="profile_type" class="col-sm-2 col-lg-1 control-label text-left">Type</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_type" ng-model="profile.Type" class="form-control">
|
||||
<option value="ssd">SSD</option>
|
||||
<option value="hdd">HDD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !type -->
|
||||
<!-- iops -->
|
||||
<div ng-if="!state.LimitBandwidth || state.NoLimit">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
IOPS
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="permissions" class="control-label text-left">
|
||||
Limit IOPS
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="state.LimitIOPS" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.LimitIOPS">
|
||||
<label for="min_iops" class="col-sm-1 control-label text-left">Min</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.min_iops.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="profile.MinIOPS" name="min_iops" ng-min="30" ng-max="999999" placeholder="100" required>
|
||||
</div>
|
||||
<label for="max_iops" class="col-sm-1 control-label text-left">Max</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.max_iops.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="profile.MaxIOPS" name="max_iops" ng-min="30" ng-max="999999" placeholder="2000" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.min_iops.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.min_iops.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min IOPS.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min IOPS: 30.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min IOPS: 999999.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.max_iops.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.max_iops.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max IOPS.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max IOPS: 30.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max IOPS: 999999.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !iops -->
|
||||
<!-- bandwidth -->
|
||||
<div ng-if="!state.LimitIOPS || state.NoLimit">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Bandwidth
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="permissions" class="control-label text-left">
|
||||
Limit bandwidth
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="state.LimitBandwidth" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.LimitBandwidth">
|
||||
<label for="min_bandwidth" class="col-sm-1 control-label text-left">Min</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.min_bandwidth.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="profile.MinBandwidth" name="min_bandwidth" ng-min="1" ng-max="5000" placeholder="1" required>
|
||||
</div>
|
||||
<label for="max_bandwidth" class="col-sm-1 control-label text-left">Max</label>
|
||||
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.max_bandwidth.$invalid }">
|
||||
<input type="number" class="form-control" ng-model="profile.MaxBandwidth" name="max_bandwidth" ng-min="1" ng-max="5000" placeholder="100" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.min_bandwidth.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.min_bandwidth.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min bandwidth.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min bandwidth: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min bandwidth: 5000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="storidgeUpdateProfileForm.max_bandwidth.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="storidgeUpdateProfileForm.max_bandwidth.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max bandwidth.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max bandwidth: 1.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max bandwidth: 5000.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !bandwidth -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="update()" ng-disabled="state.updateInProgress || !storidgeUpdateProfileForm.$valid" button-spinner="state.updateInProgress">
|
||||
<span ng-hide="state.updateInProgress">Update the profile</span>
|
||||
<span ng-show="state.updateInProgress">Updating profile...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="delete()" ng-disabled="state.deleteInProgress" button-spinner="state.deleteInProgress">
|
||||
<span ng-hide="state.deleteInProgress">Delete the profile</span>
|
||||
<span ng-show="state.deleteInProgress">Deleting profile...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,98 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('EditProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', 'ModalService',
|
||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager, ModalService) {
|
||||
|
||||
$scope.state = {
|
||||
NoLimit: false,
|
||||
LimitIOPS: false,
|
||||
LimitBandwidth: false,
|
||||
updateInProgress: false,
|
||||
deleteInProgress: false
|
||||
};
|
||||
|
||||
$scope.RedundancyOptions = [
|
||||
{ value: 2, label: '2-copy' },
|
||||
{ value: 3, label: '3-copy' }
|
||||
];
|
||||
|
||||
$scope.update = function() {
|
||||
|
||||
var profile = $scope.profile;
|
||||
|
||||
if (!$scope.state.LimitIOPS) {
|
||||
delete profile.MinIOPS;
|
||||
delete profile.MaxIOPS;
|
||||
}
|
||||
|
||||
if (!$scope.state.LimitBandwidth) {
|
||||
delete profile.MinBandwidth;
|
||||
delete profile.MaxBandwidth;
|
||||
}
|
||||
|
||||
$scope.state.updateInProgress = true;
|
||||
StoridgeProfileService.update(profile)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Profile successfully updated');
|
||||
$state.go('storidge.profiles');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update profile');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.updateInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.delete = function() {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to remove this profile?',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
deleteProfile();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function deleteProfile() {
|
||||
var profile = $scope.profile;
|
||||
|
||||
$scope.state.deleteInProgress = true;
|
||||
StoridgeProfileService.delete(profile.Name)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Profile successfully deleted');
|
||||
$state.go('storidge.profiles');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete profile');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.deleteInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
StoridgeProfileService.profile($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var profile = data;
|
||||
if ((profile.MinIOPS && profile.MinIOPS !== 0) || (profile.MaxIOPS && profile.MaxIOPS !== 0)) {
|
||||
$scope.state.LimitIOPS = true;
|
||||
} else if ((profile.MinBandwidth && profile.MinBandwidth !== 0) || (profile.MaxBandwidth && profile.MaxBandwidth !== 0)) {
|
||||
$scope.state.LimitBandwidth = true;
|
||||
} else {
|
||||
$scope.state.NoLimit = true;
|
||||
}
|
||||
$scope.profile = profile;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve profile details');
|
||||
});
|
||||
}
|
||||
|
||||
StoridgeManager.init()
|
||||
.then(function success() {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,123 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Storidge profiles">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.profiles" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.profiles">Profiles</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title="Add a profile">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="profile_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="profile_name" placeholder="e.g. myProfile">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The profile will be created using the default properties.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="create()" ng-disabled="state.actionInProgress || !formValues.Name" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Create the profile</span>
|
||||
<span ng-show="state.actionInProgress">Creating profile...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name" ui-sref="storidge.profiles.create({ profileName: formValues.Name })">Modify defaults...</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<storidge-profiles-datatable
|
||||
title="Profiles" title-icon="fa-sticky-note-o"
|
||||
dataset="profiles" table-key="storidge_profiles"
|
||||
order-by="Name" show-text-filter="true"
|
||||
remove-action="removeAction"
|
||||
></storidge-profiles-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sticky-note-o" title="Profiles">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-md-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeProfiles()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="profile in (state.filteredProfiles = (profiles | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="profile.Checked" ng-change="selectItem(profile)" /></td>
|
||||
<td>
|
||||
<a ui-sref="storidge.profiles.edit({id: profile.Name})">{{ profile.Name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!profiles">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="state.filteredProfiles.length == 0">
|
||||
<td colspan="3" class="text-center text-muted">No profiles available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="profiles" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div> -->
|
|
@ -0,0 +1,70 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', 'StoridgeManager',
|
||||
function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeManager) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.removeAction = function(selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (profile) {
|
||||
StoridgeProfileService.delete(profile.Name)
|
||||
.then(function success() {
|
||||
Notifications.success('Profile successfully removed', profile.Name);
|
||||
var index = $scope.profiles.indexOf(profile);
|
||||
$scope.profiles.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove profile');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.create = function() {
|
||||
var model = new StoridgeProfileDefaultModel();
|
||||
model.Name = $scope.formValues.Name;
|
||||
model.Directory = model.Directory + model.Name;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StoridgeProfileService.create(model)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Profile successfully created');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create profile');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
StoridgeProfileService.profiles()
|
||||
.then(function success(data) {
|
||||
$scope.profiles = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve profiles');
|
||||
});
|
||||
}
|
||||
|
||||
StoridgeManager.init()
|
||||
.then(function success() {
|
||||
initView();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to communicate with Storidge API');
|
||||
});
|
||||
}]);
|
|
@ -66,7 +66,7 @@ angular.module('portainer.filters')
|
|||
labelStyle = 'primary';
|
||||
} else if (includeString(status, ['running'])) {
|
||||
labelStyle = 'success';
|
||||
}
|
||||
}
|
||||
return labelStyle;
|
||||
};
|
||||
})
|
||||
|
@ -345,4 +345,13 @@ angular.module('portainer.filters')
|
|||
return function (createdBy) {
|
||||
return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN ');
|
||||
};
|
||||
})
|
||||
.filter('trimshasum', function () {
|
||||
'use strict';
|
||||
return function (imageName) {
|
||||
if (imageName.indexOf('sha256:') === 0) {
|
||||
return imageName.substring(7, 19);
|
||||
}
|
||||
return _.split(imageName, '@sha256')[0];
|
||||
};
|
||||
});
|
||||
|
|
|
@ -172,8 +172,7 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
|
|||
// e.g 3600000000000 nanoseconds = 1h
|
||||
|
||||
helper.translateNanosToHumanDuration = function(nanos) {
|
||||
var humanDuration = '0s';
|
||||
|
||||
var humanDuration = '0s';
|
||||
var conversionFromNano = {};
|
||||
conversionFromNano['ns'] = 1;
|
||||
conversionFromNano['us'] = conversionFromNano['ns'] * 1000;
|
||||
|
@ -186,10 +185,62 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
|
|||
if ( nanos % conversionFromNano[unit] === 0 && (nanos / conversionFromNano[unit]) > 0) {
|
||||
humanDuration = (nanos / conversionFromNano[unit]) + unit;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
return humanDuration;
|
||||
};
|
||||
|
||||
helper.translateLogDriverOptsToKeyValue = function(logOptions) {
|
||||
var options = [];
|
||||
if (logOptions) {
|
||||
Object.keys(logOptions).forEach(function(key) {
|
||||
options.push({
|
||||
key: key,
|
||||
value: logOptions[key],
|
||||
originalKey: key,
|
||||
originalValue: logOptions[key],
|
||||
added: true
|
||||
});
|
||||
});
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
helper.translateKeyValueToLogDriverOpts = function(keyValueLogDriverOpts) {
|
||||
var options = {};
|
||||
if (keyValueLogDriverOpts) {
|
||||
keyValueLogDriverOpts.forEach(function(option) {
|
||||
if (option.key && option.key !== '' && option.value && option.value !== '') {
|
||||
options[option.key] = option.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
helper.translateHostsEntriesToHostnameIP = function(entries) {
|
||||
var ipHostEntries = [];
|
||||
if (entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.indexOf(' ') && entry.split(' ').length === 2) {
|
||||
var keyValue = entry.split(' ');
|
||||
ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0]});
|
||||
}
|
||||
});
|
||||
}
|
||||
return ipHostEntries;
|
||||
};
|
||||
|
||||
helper.translateHostnameIPToHostsEntries = function(entries) {
|
||||
var ipHostEntries = [];
|
||||
if (entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.ip && entry.hostname) {
|
||||
ipHostEntries.push(entry.ip + ' ' + entry.hostname);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ipHostEntries;
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
||||
|
|
|
@ -18,7 +18,8 @@ angular.module('portainer.helpers')
|
|||
Privileged: false,
|
||||
ExtraHosts: []
|
||||
},
|
||||
Volumes: {}
|
||||
Volumes: {},
|
||||
Labels: {}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -46,6 +47,16 @@ angular.module('portainer.helpers')
|
|||
return portConfiguration;
|
||||
};
|
||||
|
||||
helper.updateContainerConfigurationWithLabels = function(labelsArray) {
|
||||
var labels = {};
|
||||
labelsArray.forEach(function (l) {
|
||||
if (l.name && l.value) {
|
||||
labels[l.name] = l.value;
|
||||
}
|
||||
});
|
||||
return labels;
|
||||
};
|
||||
|
||||
helper.EnvToStringArray = function(templateEnvironment, containerMapping) {
|
||||
var env = [];
|
||||
templateEnvironment.forEach(function(envvar) {
|
||||
|
|
|
@ -2,7 +2,7 @@ function StackViewModel(data) {
|
|||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.Checked = false;
|
||||
this.Env = data.Env;
|
||||
this.Env = data.Env ? data.Env : [];
|
||||
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ function TemplateViewModel(data) {
|
|||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Volumes = [];
|
||||
|
||||
if (data.volumes) {
|
||||
this.Volumes = data.volumes.map(function (v) {
|
||||
// @DEPRECATED: New volume definition introduced
|
||||
|
@ -43,5 +45,5 @@ function TemplateViewModel(data) {
|
|||
};
|
||||
});
|
||||
}
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
}
|
||||
|
|
|
@ -34,7 +34,5 @@ function ContainerViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
} else {
|
||||
this.ResourceControl = { Ownership: 'public' };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,5 @@ function NetworkViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
} else {
|
||||
this.ResourceControl = { Ownership: 'public' };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,15 @@ function ServiceViewModel(data, runningTasks, allTasks, nodes) {
|
|||
this.RestartMaxAttempts = 0;
|
||||
this.RestartWindow = 0;
|
||||
}
|
||||
|
||||
if (data.Spec.TaskTemplate.LogDriver) {
|
||||
this.LogDriverName = data.Spec.TaskTemplate.LogDriver.Name || '';
|
||||
this.LogDriverOpts = data.Spec.TaskTemplate.LogDriver.Options || [];
|
||||
} else {
|
||||
this.LogDriverName = '';
|
||||
this.LogDriverOpts = [];
|
||||
}
|
||||
|
||||
this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
|
||||
this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : [];
|
||||
this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : [];
|
||||
|
|
|
@ -14,7 +14,5 @@ function VolumeViewModel(data) {
|
|||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
} else {
|
||||
this.ResourceControl = { Ownership: 'public' };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,8 +167,8 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.updateStack = function(id, stackFile, env) {
|
||||
return Stack.update({ id: id, StackFileContent: stackFile, Env: env }).$promise;
|
||||
service.updateStack = function(id, stackFile, env, prune) {
|
||||
return Stack.update({ id: id, StackFileContent: stackFile, Env: env, Prune: prune}).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
|
|
|
@ -87,8 +87,8 @@ angular.module('portainer.services')
|
|||
label: 'TX on eth0',
|
||||
data: [],
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(255,180,174,0.5)',
|
||||
borderColor: 'rgba(255,180,174,0.7)',
|
||||
backgroundColor: 'rgba(255,180,174,0.4)',
|
||||
borderColor: 'rgba(255,180,174,0.6)',
|
||||
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||
pointBorderColor: 'rgba(255,180,174,1)',
|
||||
pointRadius: 2,
|
||||
|
|
|
@ -60,5 +60,9 @@ angular.module('portainer.services')
|
|||
return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0');
|
||||
};
|
||||
|
||||
service.loggingPlugins = function(systemOnly) {
|
||||
return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0');
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
angular.module('portainer.services')
|
||||
.factory('ExtensionManager', ['$q', 'PluginService', 'StoridgeManager', function ExtensionManagerFactory($q, PluginService, StoridgeManager) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.init = function() {
|
||||
return $q.all(
|
||||
StoridgeManager.init()
|
||||
);
|
||||
};
|
||||
|
||||
service.reset = function() {
|
||||
StoridgeManager.reset();
|
||||
};
|
||||
|
||||
service.extensions = function() {
|
||||
var deferred = $q.defer();
|
||||
var extensions = [];
|
||||
|
||||
PluginService.volumePlugins()
|
||||
.then(function success(data) {
|
||||
var volumePlugins = data;
|
||||
if (_.includes(volumePlugins, 'cio:latest')) {
|
||||
extensions.push('storidge');
|
||||
}
|
||||
deferred.resolve(extensions);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve extensions', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -41,6 +41,15 @@ angular.module('portainer.services')
|
|||
getPaginationLimit: function(key) {
|
||||
return localStorageService.cookie.get('pagination_' + key);
|
||||
},
|
||||
storeStoridgeAPIURL: function(url) {
|
||||
localStorageService.set('STORIDGE_API_URL', url);
|
||||
},
|
||||
getStoridgeAPIURL: function() {
|
||||
return localStorageService.get('STORIDGE_API_URL');
|
||||
},
|
||||
clearStoridgeAPIURL: function() {
|
||||
return localStorageService.remove('STORIDGE_API_URL');
|
||||
},
|
||||
getDataTableOrder: function(key) {
|
||||
return localStorageService.get('datatable_order_' + key);
|
||||
},
|
||||
|
|
|
@ -156,5 +156,19 @@ angular.module('portainer.services')
|
|||
});
|
||||
};
|
||||
|
||||
service.confirmServiceForceUpdate = function(message, callback) {
|
||||
service.confirm({
|
||||
title: 'Are you sure ?',
|
||||
message: message,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Update',
|
||||
className: 'btn-primary'
|
||||
}
|
||||
},
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
angular.module('portainer.services')
|
||||
.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService) {
|
||||
.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'ExtensionManager', 'APPLICATION_CACHE_VALIDITY', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, ExtensionManager, APPLICATION_CACHE_VALIDITY) {
|
||||
'use strict';
|
||||
|
||||
var manager = {};
|
||||
|
@ -34,6 +34,41 @@ angular.module('portainer.services')
|
|||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
function assignStateFromStatusAndSettings(status, settings) {
|
||||
state.application.authentication = status.Authentication;
|
||||
state.application.analytics = status.Analytics;
|
||||
state.application.endpointManagement = status.EndpointManagement;
|
||||
state.application.version = status.Version;
|
||||
state.application.logo = settings.LogoURL;
|
||||
state.application.displayDonationHeader = settings.DisplayDonationHeader;
|
||||
state.application.displayExternalContributors = settings.DisplayExternalContributors;
|
||||
state.application.validity = moment().unix();
|
||||
}
|
||||
|
||||
function loadApplicationState() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
settings: SettingsService.publicSettings(),
|
||||
status: StatusService.status()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var status = data.status;
|
||||
var settings = data.settings;
|
||||
assignStateFromStatusAndSettings(status, settings);
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
deferred.resolve(state);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({msg: 'Unable to retrieve server settings and status', err: err});
|
||||
})
|
||||
.finally(function final() {
|
||||
state.loading = false;
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
manager.initialize = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
|
@ -44,32 +79,28 @@ angular.module('portainer.services')
|
|||
|
||||
var applicationState = LocalStorage.getApplicationState();
|
||||
if (applicationState) {
|
||||
state.application = applicationState;
|
||||
state.loading = false;
|
||||
deferred.resolve(state);
|
||||
var now = moment().unix();
|
||||
var cacheValidity = now - applicationState.validity;
|
||||
if (cacheValidity > APPLICATION_CACHE_VALIDITY) {
|
||||
loadApplicationState()
|
||||
.then(function success(data) {
|
||||
deferred.resolve(state);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
} else {
|
||||
state.application = applicationState;
|
||||
state.loading = false;
|
||||
deferred.resolve(state);
|
||||
}
|
||||
} else {
|
||||
$q.all({
|
||||
settings: SettingsService.publicSettings(),
|
||||
status: StatusService.status()
|
||||
})
|
||||
loadApplicationState()
|
||||
.then(function success(data) {
|
||||
var status = data.status;
|
||||
var settings = data.settings;
|
||||
state.application.authentication = status.Authentication;
|
||||
state.application.analytics = status.Analytics;
|
||||
state.application.endpointManagement = status.EndpointManagement;
|
||||
state.application.version = status.Version;
|
||||
state.application.logo = settings.LogoURL;
|
||||
state.application.displayDonationHeader = settings.DisplayDonationHeader;
|
||||
state.application.displayExternalContributors = settings.DisplayExternalContributors;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
deferred.resolve(state);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({msg: 'Unable to retrieve server settings and status', err: err});
|
||||
})
|
||||
.finally(function final() {
|
||||
state.loading = false;
|
||||
deferred.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -83,13 +114,15 @@ angular.module('portainer.services')
|
|||
}
|
||||
$q.all({
|
||||
info: SystemService.info(),
|
||||
version: SystemService.version()
|
||||
version: SystemService.version(),
|
||||
extensions: ExtensionManager.extensions()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpointMode = InfoHelper.determineEndpointMode(data.info);
|
||||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||
state.endpoint.mode = endpointMode;
|
||||
state.endpoint.apiVersion = endpointAPIVersion;
|
||||
state.endpoint.extensions = data.extensions;
|
||||
LocalStorage.storeEndpointState(state.endpoint);
|
||||
deferred.resolve();
|
||||
})
|
||||
|
|
|
@ -54,6 +54,7 @@ angular.module('portainer.services')
|
|||
var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
|
||||
configuration.OpenStdin = consoleConfiguration.openStdin;
|
||||
configuration.Tty = consoleConfiguration.tty;
|
||||
configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
|
||||
return configuration;
|
||||
};
|
||||
|
||||
|
|
|
@ -79,6 +79,10 @@ a[ng-click]{
|
|||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.space-left {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tooltip.portainer-tooltip .tooltip-inner {
|
||||
font-family: Montserrat;
|
||||
background-color: #ffffff;
|
||||
|
@ -377,7 +381,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a {
|
|||
text-indent: 35px;
|
||||
font-size: 12px;
|
||||
color: #b2bfdc;
|
||||
line-height: 40px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 36px;
|
||||
}
|
||||
ul.sidebar .sidebar-title .form-control {
|
||||
height: 36px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||
|
@ -386,27 +410,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||
background: #2d3e63;
|
||||
}
|
||||
|
||||
@media (max-height: 683px) {
|
||||
@media (max-height: 785px) {
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 28px;
|
||||
line-height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-title .form-control {
|
||||
height: 28px;
|
||||
padding: 4px 8px;
|
||||
height: 26px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 28px;
|
||||
height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
line-height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
line-height: 28px;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-height: 684px) and (max-height: 850px) {
|
||||
@media(min-height: 786px) and (max-height: 924px) {
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
|
|
@ -9,3 +9,5 @@ WORKDIR /
|
|||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/portainer"]
|
||||
|
||||
HEALTHCHECK --start-period=10ms --interval=30s --timeout=5s --retries=3 CMD ["/portainer", "-c"]
|
||||
|
|
|
@ -13,9 +13,8 @@ steps:
|
|||
image: portainer/angular-builder:latest
|
||||
working_directory: ${{build_backend}}
|
||||
commands:
|
||||
- npm install -g bower grunt grunt-cli && npm install
|
||||
- bower install --allow-root
|
||||
- grunt build-webapp
|
||||
- yarn
|
||||
- yarn grunt build-webapp
|
||||
- mv api/cmd/portainer/portainer dist/
|
||||
|
||||
download_docker_binary:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Name: portainer
|
||||
Version: 1.15.5
|
||||
Version: 1.16.0
|
||||
Release: 0
|
||||
License: Zlib
|
||||
Summary: A lightweight docker management UI
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue