Merge branch 'release/1.16.0'

pull/1588/head 1.16.0
Anthony Lapenna 2018-01-21 17:30:13 +01:00
commit c40f120da2
104 changed files with 3770 additions and 183 deletions

View File

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

View File

@ -6,6 +6,7 @@ const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultCheckHealth = "false"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"

View File

@ -4,6 +4,7 @@ const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultCheckHealth = "false"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import (
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
@ -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
}

View File

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

11
api/http/health_check.go Normal file
View File

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

View File

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

View File

@ -56,7 +56,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.15.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."

View 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',

View File

@ -7,7 +7,7 @@ angular.module('portainer')
StateManager.initialize()
.then(function success(state) {
if (state.application.authentication) {
initAuthentication(authManager, Authentication, $rootScope);
initAuthentication(authManager, Authentication, $rootScope, $state);
}
if (state.application.analytics) {
initAnalytics(Analytics, $rootScope);
@ -30,7 +30,7 @@ angular.module('portainer')
}]);
function initAuthentication(authManager, Authentication, $rootScope) {
function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp; 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>

View File

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

View File

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

View File

@ -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.',

View File

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

View File

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

View File

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

View File

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

View File

@ -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' } }">

View File

@ -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');

View File

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

View File

@ -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 = '';

View File

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

View File

@ -40,5 +40,6 @@ function ($q, $scope, $state, VolumeService, Notifications) {
Notifications.error('Failure', err, 'Unable to retrieve volumes');
});
}
initView();
}]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ angular.module('ui').component('servicesDatatable', {
showOwnershipColumn: '<',
removeAction: '<',
scaleAction: '<',
swarmManagerIp: '<'
swarmManagerIp: '<',
forceUpdateAction: '<',
showForceUpdateButton: '<'
}
});

View File

@ -9,7 +9,7 @@ angular.module('ui').component('stackServicesDatatable', {
orderBy: '@',
reverseOrder: '<',
nodes: '<',
publicURL: '<',
publicUrl: '<',
showTextFilter: '<'
}
});

View File

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

View File

@ -9,6 +9,7 @@ angular.module('ui').component('usersDatatable', {
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
removeAction: '<'
removeAction: '<',
authenticationMethod: '<'
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
angular.module('extension.storidge').component('storidgeProfileSelector', {
templateUrl: 'app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html',
controller: 'StoridgeProfileSelectorController',
bindings: {
'storidgeProfile': '='
}
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
function StoridgeEventModel(data) {
this.Time = data.time;
this.Category = data.category;
this.Module = data.module;
this.Content = data.content;
}

View File

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

View File

@ -0,0 +1,6 @@
function StoridgeNodeModel(name, data) {
this.Name = name;
this.IP = data.ip;
this.Role = data.role;
this.Status = data.status;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> &gt; <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> -->

View File

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

View File

@ -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> &gt; <a ui-sref="storidge.profiles">Profiles</a> &gt; 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>

View File

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

View File

@ -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> &gt; <a ui-sref="storidge.profiles">Profiles</a> &gt; {{ 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>

View File

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

View File

@ -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> &gt; <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> -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,5 @@ function ContainerViewModel(data) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} else {
this.ResourceControl = { Ownership: 'public' };
}
}

View File

@ -19,7 +19,5 @@ function NetworkViewModel(data) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} else {
this.ResourceControl = { Ownership: 'public' };
}
}

View File

@ -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 || [] : [];

View File

@ -14,7 +14,5 @@ function VolumeViewModel(data) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} else {
this.ResourceControl = { Ownership: 'public' };
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,3 +9,5 @@ WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer"]
HEALTHCHECK --start-period=10ms --interval=30s --timeout=5s --retries=3 CMD ["/portainer", "-c"]

View File

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

View File

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