Compare commits
59 Commits
feat/EE-35
...
feat-fdo-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
540afcd179 | ||
|
|
4c23513a41 | ||
|
|
81d1f35bdc | ||
|
|
36c93c7f57 | ||
|
|
b67f404d8d | ||
|
|
95fb5a4baa | ||
|
|
dd372637cb | ||
|
|
c1a4856e9d | ||
|
|
92b7e64689 | ||
|
|
a750259a2c | ||
|
|
87accfce5d | ||
|
|
29f0daa7ea | ||
|
|
a247db7e93 | ||
|
|
1fbaf5fcbf | ||
|
|
c981e6ff7b | ||
|
|
ee1ee633d7 | ||
|
|
a7ab0a5662 | ||
|
|
bed4257194 | ||
|
|
5ee570e075 | ||
|
|
9666c21b8a | ||
|
|
5cf789a8e4 | ||
|
|
6a4a353b92 | ||
|
|
02355acfa8 | ||
|
|
04eb718f88 | ||
|
|
36888b5ad4 | ||
|
|
7bd971f838 | ||
|
|
c3ce4d8b53 | ||
|
|
fb3a31a4fd | ||
|
|
b6852b5e30 | ||
|
|
34e2178752 | ||
|
|
83a17de1c0 | ||
|
|
e5b27d7a57 | ||
|
|
fb14a85483 | ||
|
|
8d4cb5e16b | ||
|
|
ad8b8399c4 | ||
|
|
8ff2fa66b6 | ||
|
|
539948b5a6 | ||
|
|
bfe1cace77 | ||
|
|
7b806cf586 | ||
|
|
69a824c25b | ||
|
|
8d733ccc8c | ||
|
|
2574f223b4 | ||
|
|
f2d93654f5 | ||
|
|
5e74b90780 | ||
|
|
78ce176268 | ||
|
|
dfb398d091 | ||
|
|
4e9b3a8940 | ||
|
|
64b9207497 | ||
|
|
b3dbfd1a5e | ||
|
|
0f08005982 | ||
|
|
587ea7e8ea | ||
|
|
fe82b23211 | ||
|
|
6099a916b9 | ||
|
|
3d9aae9760 | ||
|
|
1316168b6b | ||
|
|
88042f9b39 | ||
|
|
4515315f41 | ||
|
|
00bd7c3d02 | ||
|
|
0b8adb690e |
1
.gitignore
vendored
@@ -7,6 +7,7 @@ storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
.vscode
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
|
||||
76
api/agent/version.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
netUrl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// GetAgentVersionAndPlatform returns the agent version and platform
|
||||
//
|
||||
// it sends a ping to the agent and parses the version and platform from the headers
|
||||
func GetAgentVersionAndPlatform(url string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.Contains(url, "://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
parsedURL, err := netUrl.Parse(fmt.Sprintf("%s/ping", url))
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
parsedURL.Scheme = "https"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
version := resp.Header.Get(portainer.PortainerAgentHeader)
|
||||
if version == "" {
|
||||
return 0, "", errors.New("Version Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, "", errors.New("Agent Platform Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if agentPlatformNumber == 0 {
|
||||
return 0, "", errors.New("Agent platform is invalid")
|
||||
}
|
||||
|
||||
return portainer.AgentPlatform(agentPlatformNumber), version, nil
|
||||
}
|
||||
@@ -27,6 +27,9 @@
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"Agent": {
|
||||
"Version": ""
|
||||
},
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"AzureCredentials": {
|
||||
|
||||
@@ -2,11 +2,13 @@ package fdo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -38,6 +40,10 @@ type ServiceInfo struct {
|
||||
func (c FDOOwnerClient) doDigestAuthReq(method, endpoint, contentType string, body io.Reader) (*http.Response, error) {
|
||||
transport := digest.NewTransport(c.Username, c.Password)
|
||||
|
||||
// TODO: REVIEW
|
||||
// Temporary work-around to support sending requests to HTTPS AIO local setups
|
||||
transport.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
client, err := transport.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -70,7 +76,7 @@ func (c FDOOwnerClient) PostVoucher(ov []byte) (string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPost,
|
||||
"api/v1/owner/vouchers",
|
||||
"application/cbor",
|
||||
"text/plain",
|
||||
bytes.NewReader(ov),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -90,25 +96,69 @@ func (c FDOOwnerClient) PostVoucher(ov []byte) (string, error) {
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
values := url.Values{}
|
||||
values.Set("module", info.Module)
|
||||
values.Set("var", info.Var)
|
||||
values.Set("filename", info.Filename)
|
||||
values.Set("guid", info.GUID)
|
||||
values.Set("device", info.Device)
|
||||
values.Set("priority", strconv.Itoa(info.Priority))
|
||||
values.Set("os", info.OS)
|
||||
values.Set("version", info.Version)
|
||||
values.Set("arch", info.Arch)
|
||||
values.Set("crid", strconv.Itoa(info.CRID))
|
||||
values.Set("hash", info.Hash)
|
||||
// func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
// values := url.Values{}
|
||||
// values.Set("module", info.Module)
|
||||
// values.Set("var", info.Var)
|
||||
// values.Set("filename", info.Filename)
|
||||
// values.Set("guid", info.GUID)
|
||||
// values.Set("device", info.Device)
|
||||
// values.Set("priority", strconv.Itoa(info.Priority))
|
||||
// values.Set("os", info.OS)
|
||||
// values.Set("version", info.Version)
|
||||
// values.Set("arch", info.Arch)
|
||||
// values.Set("crid", strconv.Itoa(info.CRID))
|
||||
// values.Set("hash", info.Hash)
|
||||
|
||||
// resp, err := c.doDigestAuthReq(
|
||||
// http.MethodPut,
|
||||
// "api/v1/device/svi?"+values.Encode(),
|
||||
// "application/octet-stream",
|
||||
// strings.NewReader(string(info.Bytes)),
|
||||
// )
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
|
||||
// if resp.StatusCode != http.StatusOK {
|
||||
// return errors.New(http.StatusText(resp.StatusCode))
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Sending SVI instruction
|
||||
// curl -D - --digest -u ${api_user}: --location --request POST 'http://localhost:8080/api/v1/owner/svi' --header 'Content-Type: text/plain' --data-raw '[{"filedesc" : "setup.sh","resource" : "URL"}, {"write": "content_string"} {"exec" : ["bash","setup.sh"] }]'
|
||||
|
||||
// Uploading resources
|
||||
// curl -D - --digest -u ${api_user}: --location --request POST 'http://localhost:8080/api/v1/owner/resource?filename=fileName' --header 'Content-Type: text/plain' --data-binary '@< path to file >'
|
||||
|
||||
func SVIInstructionsToString(SVIInstructions []json.RawMessage) (string, error) {
|
||||
data, err := json.Marshal(SVIInstructions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// curl -k -D - --digest -u apiUser:U8MdQyV7W9TUXtG --location --request POST 'https://localhost:8443/api/v1/owner/svi' \
|
||||
// --header 'Content-Type: text/plain' \
|
||||
// --data-raw '[{"filedesc" : "sample.txt","resource" : "file.txt"}]'
|
||||
|
||||
// curl -k -D - --digest -u apiUser:U8MdQyV7W9TUXtG --location --request POST 'https://localhost:8443/api/v1/owner/resource?filename=file.txt' --header 'Content-Type: text/plain' --data-binary '@/tmp/file.txt'
|
||||
|
||||
func (c FDOOwnerClient) PostResource(fileName string, resourceContent []byte) error {
|
||||
params := url.Values{
|
||||
"filename": []string{fileName},
|
||||
}
|
||||
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+values.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(info.Bytes)),
|
||||
http.MethodPost,
|
||||
"api/v1/owner/resource?"+params.Encode(),
|
||||
"text/plain",
|
||||
bytes.NewReader(resourceContent),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -122,12 +172,28 @@ func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
// TODO: REVIEW
|
||||
// Review comments
|
||||
|
||||
// POST a SVI to the Owner service
|
||||
// That SVI will instruct the device to retrieve a file called resourceName from the database and write it under the fileName path (in local process CWD? e.g. in agent CWD - not sure about that)
|
||||
|
||||
// To FileName
|
||||
// From ResourceName
|
||||
func (c FDOOwnerClient) PostSVI(fileName, resourceName string) error {
|
||||
op1 := json.RawMessage([]byte(fmt.Sprintf(`{"filedesc" : "%s", "resource": "%s"}`, fileName, resourceName)))
|
||||
|
||||
data := []json.RawMessage{op1}
|
||||
payload, err := SVIInstructionsToString(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+info.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(body)),
|
||||
http.MethodPost,
|
||||
"api/v1/owner/svi",
|
||||
"text/plain",
|
||||
bytes.NewReader([]byte(payload)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -141,6 +207,59 @@ func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST a SVI to the Owner service
|
||||
// That SVI will instruct the device to retrieve a file called resourceName from the database and write it under the fileName path (in local process CWD? e.g. in agent CWD - not sure about that)
|
||||
// It will then instruct the device to execute a command afterwards
|
||||
|
||||
// To FileName
|
||||
// From ResourceName
|
||||
// func (c FDOOwnerClient) PostSVIFileExec(fileName, resourceName string, execCommand []string) error {
|
||||
// op1 := json.RawMessage([]byte(fmt.Sprintf(`{"filedesc" : "%s", "resource": "%s"}`, fileName, resourceName)))
|
||||
// op2 := json.RawMessage([]byte(fmt.Sprintf(`{"exec" : ["%s"]}`, strings.Join(execCommand, "\",\""))))
|
||||
|
||||
// data := []json.RawMessage{op1, op2}
|
||||
// payload, err := SVIInstructionsToString(data)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// resp, err := c.doDigestAuthReq(
|
||||
// http.MethodPost,
|
||||
// "api/v1/owner/svi",
|
||||
// "text/plain",
|
||||
// strings.NewReader(payload),
|
||||
// )
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
|
||||
// if resp.StatusCode != http.StatusOK {
|
||||
// return errors.New(http.StatusText(resp.StatusCode))
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
// resp, err := c.doDigestAuthReq(
|
||||
// http.MethodPut,
|
||||
// "api/v1/device/svi?"+info.Encode(),
|
||||
// "application/octet-stream",
|
||||
// strings.NewReader(string(body)),
|
||||
// )
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
|
||||
// if resp.StatusCode != http.StatusOK {
|
||||
// return errors.New(http.StatusText(resp.StatusCode))
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodGet,
|
||||
|
||||
@@ -8,11 +8,18 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
addrFormatFQDN = 201
|
||||
addrFormatIpv4 = 3
|
||||
addrFormatIpv6 = 4
|
||||
)
|
||||
|
||||
type CIRAConfig struct {
|
||||
ConfigName string `json:"configName"`
|
||||
MPSServerAddress string `json:"mpsServerAddress"`
|
||||
@@ -70,15 +77,15 @@ func (service *Service) saveCIRAConfig(method string, configuration portainer.Op
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addressFormat, err := addressFormat(configuration.MPSServer)
|
||||
addressFormat, serverAddress, err := addressFormat(configuration.MPSServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := CIRAConfig{
|
||||
ConfigName: configName,
|
||||
MPSServerAddress: configuration.MPSServer,
|
||||
CommonName: configuration.MPSServer,
|
||||
MPSServerAddress: serverAddress,
|
||||
CommonName: serverAddress,
|
||||
ServerAddressFormat: addressFormat,
|
||||
MPSPort: 4433,
|
||||
Username: "admin",
|
||||
@@ -101,18 +108,33 @@ func (service *Service) saveCIRAConfig(method string, configuration portainer.Op
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func addressFormat(url string) (int, error) {
|
||||
ip := net.ParseIP(url)
|
||||
if ip == nil {
|
||||
return 201, nil // FQDN
|
||||
// addressFormat returns the address format and the address for the given server address.
|
||||
// when using a IP:PORT format, only the IP is returned.
|
||||
// see https://github.com/open-amt-cloud-toolkit/rps/blob/b63e0112f8a6323764742165a2cd5b465d9a9a24/src/routes/admin/ciraconfig/ciraValidator.ts#L20-L25
|
||||
func addressFormat(u string) (int, string, error) {
|
||||
ip2 := net.ParseIP(u)
|
||||
if ip2 != nil {
|
||||
if ip2.To4() != nil {
|
||||
return addrFormatIpv4, u, nil
|
||||
}
|
||||
|
||||
return addrFormatIpv6, u, nil
|
||||
}
|
||||
if strings.Contains(url, ".") {
|
||||
return 3, nil // IPV4
|
||||
|
||||
_, err := url.Parse(u)
|
||||
if err == nil {
|
||||
return addrFormatFQDN, u, nil
|
||||
}
|
||||
if strings.Contains(url, ":") {
|
||||
return 4, nil // IPV6
|
||||
|
||||
host, _, err := net.SplitHostPort(u)
|
||||
if err == nil {
|
||||
if strings.Count(u, ":") >= 2 {
|
||||
return addrFormatIpv6, host, nil
|
||||
}
|
||||
return addrFormatIpv4, host, nil
|
||||
}
|
||||
return 0, fmt.Errorf("could not determine server address format for %s", url)
|
||||
|
||||
return 0, "", fmt.Errorf("could not determine server address format for %s", u)
|
||||
}
|
||||
|
||||
func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfiguration) (string, error) {
|
||||
|
||||
86
api/http/handler/docker/containers/container_gpus_inspect.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type containerGpusResponse struct {
|
||||
Gpus string `json:"gpus"`
|
||||
}
|
||||
|
||||
// @id dockerContainerGpusInspect
|
||||
// @summary Fetch container gpus data
|
||||
// @description
|
||||
// @description **Access policy**:
|
||||
// @tags docker
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param environmentId path int true "Environment identifier"
|
||||
// @param containerId path int true "Container identifier"
|
||||
// @success 200 {object} containerGpusResponse "Success"
|
||||
// @failure 404 "Environment or container not found"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 500 "Internal server error"
|
||||
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
|
||||
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid container identifier route variable", err)
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||
}
|
||||
|
||||
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
|
||||
|
||||
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
|
||||
}
|
||||
|
||||
container, err := cli.ContainerInspect(r.Context(), containerId)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to find the container", err)
|
||||
}
|
||||
|
||||
if container.HostConfig == nil {
|
||||
return httperror.NotFound("Unable to find the container host config", err)
|
||||
}
|
||||
|
||||
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
|
||||
if opt.Driver == "nvidia" {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return opt.Capabilities[0][0] == "gpu"
|
||||
})
|
||||
|
||||
if gpuOptionsIndex == -1 {
|
||||
return response.JSON(w, containerGpusResponse{Gpus: "none"})
|
||||
}
|
||||
|
||||
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
|
||||
|
||||
gpu := "all"
|
||||
if gpuOptions.Count != -1 {
|
||||
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
|
||||
}
|
||||
|
||||
return response.JSON(w, containerGpusResponse{Gpus: gpu})
|
||||
}
|
||||
31
api/http/handler/docker/containers/handler.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
|
||||
router := h.PathPrefix(routePrefix).Subrouter()
|
||||
router.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
63
api/http/handler/docker/handler.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/containers"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
dataStore dataservices.DataStore
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
authorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
authorizationService: authorizationService,
|
||||
dataStore: dataStore,
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
|
||||
// endpoints
|
||||
endpointRouter := h.PathPrefix("/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
endpointRouter.Use(dockerOnlyMiddleware)
|
||||
|
||||
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
|
||||
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
|
||||
return h
|
||||
}
|
||||
|
||||
func dockerOnlyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
|
||||
endpoint, err := middlewares.FetchEndpoint(request)
|
||||
if err != nil {
|
||||
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !endpointutils.IsDockerEndpoint(endpoint) {
|
||||
errMessage := "environment is not a docker environment"
|
||||
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(rw, request)
|
||||
})
|
||||
}
|
||||
@@ -77,14 +77,17 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
if endpoint.EdgeID == "" {
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
}
|
||||
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
|
||||
version := r.Header.Get(portainer.PortainerAgentHeader)
|
||||
endpoint.Agent.Version = version
|
||||
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
|
||||
@@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 2,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
@@ -194,7 +194,9 @@ func TestWithEndpoints(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -239,6 +241,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -355,6 +358,7 @@ func TestEdgeStackStatus(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -424,6 +428,7 @@ func TestEdgeJobsResponse(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
50
api/http/handler/endpoints/endpoint_agent_versions.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
)
|
||||
|
||||
// @id AgentVersions
|
||||
// @summary List agent versions
|
||||
// @description List all agent versions based on the current user authorizations and query parameters.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} string "List of available agent versions"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/agent_versions [get]
|
||||
|
||||
func (handler *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
agentVersions := set.Set[string]{}
|
||||
for _, endpoint := range filteredEndpoints {
|
||||
if endpoint.Agent.Version != "" {
|
||||
agentVersions[endpoint.Agent.Version] = true
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, agentVersions.Keys())
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/agent"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
@@ -245,6 +244,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
var err error
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
return handler.createAzureEndpoint(payload)
|
||||
@@ -257,12 +257,25 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
var agentVersion string
|
||||
if payload.EndpointCreationType == agentEnvironment {
|
||||
agentPlatform, err := handler.pingAndCheckPlatform(payload)
|
||||
|
||||
payload.URL = "tcp://" + normalizeAgentAddress(payload.URL)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if payload.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to create TLS configuration", err)
|
||||
}
|
||||
}
|
||||
|
||||
agentPlatform, version, err := agent.GetAgentVersionAndPlatform(payload.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
|
||||
}
|
||||
|
||||
agentVersion = version
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
@@ -272,7 +285,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType)
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
@@ -444,7 +457,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
@@ -467,6 +480,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
||||
err := handler.storeTLSFiles(endpoint, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -560,58 +575,3 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
url.Scheme = "https"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, errors.New("Agent Platform Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if agentPlatformNumber == 0 {
|
||||
return 0, errors.New("Agent platform is invalid")
|
||||
}
|
||||
|
||||
return portainer.AgentPlatform(agentPlatformNumber), nil
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const (
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
|
||||
// @param agentVersions query []string false "will return only environments with on of these agent versions"
|
||||
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
|
||||
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
|
||||
// @param name query string false "will return only environments(endpoints) with this name"
|
||||
|
||||
@@ -21,6 +21,89 @@ type endpointListTest struct {
|
||||
expected []portainer.EndpointID
|
||||
}
|
||||
|
||||
func Test_EndpointList_AgentVersion(t *testing.T) {
|
||||
|
||||
version1Endpoint := portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1, Type: portainer.AgentOnDockerEnvironment, Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "2.0.0"}}
|
||||
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
|
||||
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
|
||||
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
notAgentEnvironments,
|
||||
version1Endpoint,
|
||||
version2Endpoint,
|
||||
noVersionEndpoint,
|
||||
})
|
||||
|
||||
defer teardown()
|
||||
|
||||
type endpointListAgentVersionTest struct {
|
||||
endpointListTest
|
||||
filter []string
|
||||
}
|
||||
|
||||
tests := []endpointListAgentVersionTest{
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 1 agent endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version1Endpoint.ID, notAgentEnvironments.ID},
|
||||
},
|
||||
[]string{version1Endpoint.Agent.Version},
|
||||
},
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 2 endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID},
|
||||
},
|
||||
[]string{version2Endpoint.Agent.Version},
|
||||
},
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 1 and 2 endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID, version1Endpoint.ID},
|
||||
},
|
||||
[]string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
query := ""
|
||||
for _, filter := range test.filter {
|
||||
query += fmt.Sprintf("agentVersions[]=%s&", filter)
|
||||
}
|
||||
|
||||
req := buildEndpointListRequest(query)
|
||||
|
||||
resp, err := doEndpointListRequest(req, handler, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range resp {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.ElementsMatch(test.expected, respIds)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
@@ -48,7 +131,7 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
tests := []endpointListEdgeDeviceTest{
|
||||
{
|
||||
endpointListTest: endpointListTest{
|
||||
"should show all endpoints expect of the untrusted devices",
|
||||
"should show all endpoints except of the untrusted devices",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: nil,
|
||||
|
||||
@@ -55,6 +55,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,6 +47,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -105,7 +105,12 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.URL != nil {
|
||||
endpoint.URL = *payload.URL
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
endpoint.URL = normalizeAgentAddress(*payload.URL)
|
||||
} else {
|
||||
endpoint.URL = *payload.URL
|
||||
}
|
||||
}
|
||||
|
||||
if payload.PublicURL != nil {
|
||||
|
||||
@@ -25,6 +25,7 @@ type EnvironmentsQuery struct {
|
||||
edgeDevice *bool
|
||||
edgeDeviceUntrusted bool
|
||||
name string
|
||||
agentVersions []string
|
||||
}
|
||||
|
||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
@@ -60,6 +61,8 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
agentVersions := getArrayQueryParameter(r, "agentVersions")
|
||||
|
||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||
|
||||
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
|
||||
@@ -82,6 +85,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
edgeDevice: edgeDevice,
|
||||
edgeDeviceUntrusted: edgeDeviceUntrusted,
|
||||
name: name,
|
||||
agentVersions: agentVersions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -135,6 +139,12 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
|
||||
}
|
||||
|
||||
if len(query.agentVersions) > 0 {
|
||||
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
|
||||
return !endpointutils.IsAgentEndpoint(&endpoint) || contains(query.agentVersions, endpoint.Agent.Version)
|
||||
})
|
||||
}
|
||||
|
||||
return filteredEndpoints, totalAvailableEndpoints, nil
|
||||
}
|
||||
|
||||
@@ -413,3 +423,13 @@ func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func contains(strings []string, param string) bool {
|
||||
for _, str := range strings {
|
||||
if str == param {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,64 @@ type filterTest struct {
|
||||
query EnvironmentsQuery
|
||||
}
|
||||
|
||||
func Test_Filter_AgentVersion(t *testing.T) {
|
||||
|
||||
version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "1.0.0"}}
|
||||
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "2.0.0"}}
|
||||
noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
}
|
||||
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
version1Endpoint,
|
||||
version2Endpoint,
|
||||
noVersionEndpoint,
|
||||
notAgentEnvironments,
|
||||
}
|
||||
|
||||
handler, teardown := setupFilterTest(t, endpoints)
|
||||
|
||||
defer teardown()
|
||||
|
||||
tests := []filterTest{
|
||||
{
|
||||
"should show version 1 endpoints",
|
||||
[]portainer.EndpointID{version1Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version1Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show version 2 endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version2Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show version 1 and 2 endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTests(tests, t, handler, endpoints)
|
||||
}
|
||||
|
||||
func Test_Filter_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
|
||||
@@ -67,6 +67,9 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/agent_versions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
|
||||
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
package endpoints
|
||||
|
||||
import "strings"
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
}
|
||||
|
||||
func normalizeAgentAddress(url string) string {
|
||||
// Case insensitive strip http or https scheme if URL entered
|
||||
index := strings.Index(url, "://")
|
||||
if index >= 0 {
|
||||
return url[index+3:]
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/docker"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -45,6 +46,7 @@ type Handler struct {
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHandler *docker.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package fdo
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -17,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentScriptName = "fdo.sh"
|
||||
deploymentScriptName = "fdo-profile.sh"
|
||||
)
|
||||
|
||||
type deviceConfigurePayload struct {
|
||||
@@ -95,94 +91,173 @@ func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// enable fdo_sys
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"0"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"active"},
|
||||
"bytes": []string{"F5"}, // this is "true" in CBOR
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
// TODO: REVIEW
|
||||
// This might not be needed anymore
|
||||
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"0"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"active"},
|
||||
// "bytes": []string{"F5"}, // this is "true" in CBOR
|
||||
// }, []byte("")); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
// }
|
||||
|
||||
// write down the edge id
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"1"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"filedesc"},
|
||||
// "filename": []string{"DEVICE_edgeid.txt"},
|
||||
// }, []byte(payload.EdgeID)); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgeid)", Err: err}
|
||||
// }
|
||||
|
||||
const deviceConfiguration = `
|
||||
GUID=%s
|
||||
DEVICE_NAME=%s
|
||||
EDGE_ID=%s
|
||||
EDGE_KEY=%s
|
||||
`
|
||||
|
||||
deviceConfData := fmt.Sprintf(deviceConfiguration, guid, payload.Name, payload.EdgeID, payload.EdgeKey)
|
||||
deviceConfResourceName := fmt.Sprintf("%s-agent.conf", guid)
|
||||
|
||||
err = fdoClient.PostResource(deviceConfResourceName, []byte(deviceConfData))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: UploadResource(DEVICE.conf)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: UploadResource(DEVICE.conf)", Err: err}
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgeid.txt"},
|
||||
}, []byte(payload.EdgeID)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgeid)", Err: err}
|
||||
}
|
||||
// TODO: REVIEW
|
||||
// I believe this will need a filter on GUID
|
||||
// https://github.com/secure-device-onboard/pri-fidoiot/tree/1.1.0-rel/component-samples/demo/aio#service-info-filters
|
||||
|
||||
// err = fdoClient.PostSVIFile("DEVICE.conf", deviceConfResourceName)
|
||||
|
||||
// TODO: REVIEW whether the $guid shortcut works
|
||||
// If that's the case - then potentially we only need to do this once
|
||||
// and just do a PostResource here (the one above)
|
||||
// That would mean that we would need to relocate this action to somewhere else (a setup process after enabling the FDO integration for example)
|
||||
|
||||
// err = fdoClient.PostSVI("DEVICE.conf", "$(guid)-DEVICE.conf")
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PostSVIFile(DEVICE.conf)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PostSVIFile(DEVICE.conf)", Err: err}
|
||||
// }
|
||||
|
||||
// write down the edgekey
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgekey.txt"},
|
||||
}, []byte(payload.EdgeKey)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgekey)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgekey)", Err: err}
|
||||
}
|
||||
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"1"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"filedesc"},
|
||||
// "filename": []string{"DEVICE_edgekey.txt"},
|
||||
// }, []byte(payload.EdgeKey)); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(edgekey)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(edgekey)", Err: err}
|
||||
// }
|
||||
|
||||
// err = fdoClient.PostSVIFile("DEVICE_edgekey.txt", payload.EdgeKey)
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PostSVIFile(edgekey)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PostSVIFile(edgekey)", Err: err}
|
||||
// }
|
||||
|
||||
// write down the device name
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_name.txt"},
|
||||
}, []byte(payload.Name)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(name)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(name)", Err: err}
|
||||
}
|
||||
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"1"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"filedesc"},
|
||||
// "filename": []string{"DEVICE_name.txt"},
|
||||
// }, []byte(payload.Name)); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw(name)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw(name)", Err: err}
|
||||
// }
|
||||
|
||||
// err = fdoClient.PostSVIFile("DEVICE_name.txt", payload.Name)
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PostSVIFile(name)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PostSVIFile(name)", Err: err}
|
||||
// }
|
||||
|
||||
// write down the device GUID - used as the EDGE_DEVICE_GUID too
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_GUID.txt"},
|
||||
}, []byte(guid)); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{deploymentScriptName},
|
||||
}, fileContent); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"1"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"filedesc"},
|
||||
// "filename": []string{"DEVICE_GUID.txt"},
|
||||
// }, []byte(guid)); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
// }
|
||||
|
||||
b, err := cbor.Marshal([]string{"/bin/sh", deploymentScriptName})
|
||||
// err = fdoClient.PostSVIFile("DEVICE_GUID.txt", guid)
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PostSVIFile(guid)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PostSVIFile(guid)", Err: err}
|
||||
// }
|
||||
|
||||
// write down the profile script
|
||||
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"1"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"filedesc"},
|
||||
// "filename": []string{deploymentScriptName},
|
||||
// }, fileContent); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
// }
|
||||
|
||||
// b, err := cbor.Marshal([]string{"/bin/sh", deploymentScriptName})
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Error("failed to marshal string to CBOR")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw() failed to encode", Err: err}
|
||||
// }
|
||||
|
||||
// cborBytes := strings.ToUpper(hex.EncodeToString(b))
|
||||
// logrus.WithField("cbor", cborBytes).WithField("string", deploymentScriptName).Info("converted to CBOR")
|
||||
|
||||
// if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
// "guid": []string{guid},
|
||||
// "priority": []string{"2"},
|
||||
// "module": []string{"fdo_sys"},
|
||||
// "var": []string{"exec"},
|
||||
// "bytes": []string{cborBytes},
|
||||
// }, []byte("")); err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
// }
|
||||
|
||||
// TODO: REVIEW
|
||||
// The PostResource and PostSVI steps probably can be done once just after creating the profile
|
||||
// PostResource should also be done after removing a profile
|
||||
// DelResource should also be done after deleting a profile
|
||||
err = fdoClient.PostResource(deploymentScriptName, fileContent)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to marshal string to CBOR")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw() failed to encode", Err: err}
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: UploadResource(DEVICE.conf)")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: UploadResource(DEVICE.conf)", Err: err}
|
||||
}
|
||||
|
||||
cborBytes := strings.ToUpper(hex.EncodeToString(b))
|
||||
logrus.WithField("cbor", cborBytes).WithField("string", deploymentScriptName).Info("converted to CBOR")
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"2"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"exec"},
|
||||
"bytes": []string{cborBytes},
|
||||
}, []byte("")); err != nil {
|
||||
logrus.WithError(err).Info("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PutDeviceSVIRaw()", Err: err}
|
||||
}
|
||||
// TODO: REVIEW
|
||||
// Must be named OS_Install.sh to work with the BMO AIO setup
|
||||
// This might need to be configurable in the future
|
||||
// err = fdoClient.PostSVI("OS_Install.sh", deploymentScriptName)
|
||||
// if err != nil {
|
||||
// logrus.WithError(err).Info("fdoConfigureDevice: PostSVIFileExec(profile)")
|
||||
// return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "fdoConfigureDevice: PostSVIFileExec(profile)", Err: err}
|
||||
// }
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (handler *Handler) newFDOClient() (fdo.FDOOwnerClient, error) {
|
||||
OwnerURL: settings.FDOConfiguration.OwnerURL,
|
||||
Username: settings.FDOConfiguration.OwnerUsername,
|
||||
Password: settings.FDOConfiguration.OwnerPassword,
|
||||
Timeout: 5 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,19 @@ func (handler *Handler) openAMTActivate(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve AMT information", Err: err}
|
||||
}
|
||||
if hostInfo.ControlModeRaw < 1 {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to activate device", Err: errors.New("failed to activate device")}
|
||||
}
|
||||
|
||||
// TODO: REVIEW
|
||||
// Should check here that the activation is OK (can check for control mode / RAS remote status)
|
||||
// If not, ask the user to check the logs
|
||||
|
||||
// We should also check for the following logs in the service container
|
||||
// INFO[0050] Status: Admin control mode., MEBx Password updated
|
||||
// INFO[0050] Network: Ethernet Configured.
|
||||
// INFO[0050] CIRA: Configured
|
||||
// without the 0050 as it might change at runtime
|
||||
// if these logs are not found, assume an error and don't remove the service container as it will be useful for troubleshooting
|
||||
// consider redirecting these logs to portainer
|
||||
|
||||
if hostInfo.UUID == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve device UUID", Err: errors.New("unable to retrieve device UUID")}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@@ -25,19 +26,18 @@ import (
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
EndpointID portainer.EndpointID `json:"EndpointID"`
|
||||
RawOutput string `json:"RawOutput"`
|
||||
AMT string `json:"AMT"`
|
||||
UUID string `json:"UUID"`
|
||||
DNSSuffix string `json:"DNS Suffix"`
|
||||
BuildNumber string `json:"Build Number"`
|
||||
ControlMode string `json:"Control Mode"`
|
||||
ControlModeRaw int `json:"Control Mode (Raw)"`
|
||||
EndpointID portainer.EndpointID `json:"EndpointID"`
|
||||
RawOutput string `json:"RawOutput"`
|
||||
AMT string `json:"AMT"`
|
||||
UUID string `json:"UUID"`
|
||||
DNSSuffix string `json:"DNS Suffix"`
|
||||
BuildNumber string `json:"Build Number"`
|
||||
ControlMode string `json:"Control Mode"`
|
||||
}
|
||||
|
||||
const (
|
||||
// TODO: this should get extracted to some configurable - don't assume Docker Hub is everyone's global namespace, or that they're allowed to pull images from the internet
|
||||
rpcGoImageName = "ptrrd/openamt:rpc-go-json"
|
||||
rpcGoImageName = "intel/oact-rpc-go:v2.1.0"
|
||||
rpcGoContainerName = "openamt-rpc-go"
|
||||
dockerClientTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
dockerhandler "github.com/portainer/portainer/api/http/handler/docker"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -184,6 +185,8 @@ func (server *Server) Start() error {
|
||||
|
||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
|
||||
|
||||
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
|
||||
|
||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
|
||||
@@ -275,6 +278,7 @@ func (server *Server) Start() error {
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
|
||||
40
api/internal/set/set.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package set
|
||||
|
||||
type SetKey interface {
|
||||
~int | ~string
|
||||
}
|
||||
|
||||
type Set[T SetKey] map[T]bool
|
||||
|
||||
func (s Set[T]) Add(key T) {
|
||||
s[key] = true
|
||||
}
|
||||
|
||||
func (s Set[T]) Contains(key T) bool {
|
||||
_, ok := s[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s Set[T]) Remove(key T) {
|
||||
delete(s, key)
|
||||
}
|
||||
|
||||
func (s Set[T]) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s Set[T]) IsEmpty() bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
|
||||
func (s Set[T]) Keys() []T {
|
||||
keys := make([]T, s.Len())
|
||||
|
||||
i := 0
|
||||
for k := range s {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
@@ -2,11 +2,14 @@ package snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/agent"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
@@ -87,6 +90,24 @@ func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool {
|
||||
// SnapshotEndpoint will create a snapshot of the environment(endpoint) based on the environment(endpoint) type.
|
||||
// If the snapshot is a success, it will be associated to the environment(endpoint).
|
||||
func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error {
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
var err error
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, version, err := agent.GetAgentVersionAndPlatform(endpoint.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = version
|
||||
}
|
||||
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
return nil
|
||||
@@ -175,6 +196,7 @@ func (service *Service) snapshotEndpoints() error {
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -359,6 +359,10 @@ type (
|
||||
CommandInterval int `json:"CommandInterval" example:"60"`
|
||||
}
|
||||
|
||||
Agent struct {
|
||||
Version string `example:"1.0.0"`
|
||||
}
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<button type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
|
||||
<i style="margin: 0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
|
||||
<button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope">
|
||||
<pr-icon icon="'upload'" feather="true"></pr-icon>
|
||||
</button>
|
||||
<button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress"> </button>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
<pr-icon icon="'search'" feather="true"></pr-icon>
|
||||
<pr-icon icon="'search'" feather="true" class-name="'searchIcon'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput"
|
||||
@@ -62,7 +64,7 @@
|
||||
</tr>
|
||||
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
|
||||
<td>
|
||||
<span ng-if="item.edit">
|
||||
<span ng-if="item.edit" class="vertical-center">
|
||||
<input
|
||||
class="input-sm"
|
||||
type="text"
|
||||
@@ -71,14 +73,12 @@
|
||||
auto-focus
|
||||
/>
|
||||
<a class="interactive" ng-click="item.edit = false;"><pr-icon icon="'x'" feather="true"></pr-icon></a>
|
||||
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"
|
||||
><pr-icon icon="'check-square'" feather="true"></pr-icon
|
||||
></a>
|
||||
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><pr-icon icon="'check'" feather="true"></pr-icon></a>
|
||||
</span>
|
||||
<span ng-if="!item.edit && item.Dir">
|
||||
<a ng-click="$ctrl.browse({name: item.Name})"><pr-icon icon="'folder'" feather="true" class-name="space-right"></pr-icon>{{ item.Name }}</a>
|
||||
<a ng-click="$ctrl.browse({name: item.Name})" class="vertical-center"><pr-icon icon="'folder'" feather="true"></pr-icon>{{ item.Name }}</a>
|
||||
</span>
|
||||
<span ng-if="!item.edit && !item.Dir"><pr-icon icon="'file'" feather="true" class-name="space-right"></pr-icon>{{ item.Name }} </span>
|
||||
<span ng-if="!item.edit && !item.Dir" class="vertical-center"><pr-icon icon="'file'" feather="true"></pr-icon>{{ item.Name }}</span>
|
||||
</td>
|
||||
<td>{{ item.Size | humansize }}</td>
|
||||
<td>
|
||||
|
||||
@@ -40,15 +40,16 @@ body,
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.white-space-normal {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
max-width: 155px;
|
||||
max-height: 55px;
|
||||
}
|
||||
|
||||
.white-space-normal {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.legend .title {
|
||||
padding: 0 0.3em;
|
||||
margin: 0.5em;
|
||||
@@ -81,16 +82,16 @@ body,
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header_title_content {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
@apply text-gray-9;
|
||||
@apply th-dark:text-gray-5;
|
||||
@apply th-highcontrast:text-white;
|
||||
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-form-section-title-color);
|
||||
padding-left: 0;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-horizontal .control-label.text-left {
|
||||
@@ -150,19 +151,6 @@ a[ng-click] {
|
||||
background-color: var(--bg-item-highlighted-null-color);
|
||||
}
|
||||
|
||||
.service-datatable {
|
||||
background-color: var(--bg-item-highlighted-color);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.service-datatable thead {
|
||||
background-color: var(--bg-service-datatable-thead) !important;
|
||||
}
|
||||
|
||||
.service-datatable tbody {
|
||||
background-color: var(--bg-service-datatable-tbody);
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
@@ -881,3 +869,19 @@ json-tree .branch-preview {
|
||||
color: var(--text-link-hover-color);
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
reach-portal > div {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
input[style*='background-image: url("data:image/png'] + [data-cy='auth-passwordInputToggle'] {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
input[style*='background-image: url("data:image/png'] {
|
||||
padding-right: 60px;
|
||||
}
|
||||
|
||||
.web-editor .trancluded-item:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
51
app/assets/css/bootstrap-override.css
vendored
@@ -16,11 +16,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
color: var(--ui-gray-9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -178,8 +173,16 @@ input:checked + .slider:before {
|
||||
|
||||
/* Widget */
|
||||
|
||||
.widget .widget-icon i {
|
||||
color: var(--ui-blue-8);
|
||||
.widget .widget-icon {
|
||||
@apply text-lg !p-2 mr-1;
|
||||
@apply bg-blue-3 text-blue-8;
|
||||
@apply th-dark:bg-gray-9 th-dark:text-blue-3;
|
||||
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5%;
|
||||
}
|
||||
|
||||
.widget .widget-body table thead {
|
||||
@@ -190,16 +193,20 @@ input:checked + .slider:before {
|
||||
|
||||
#toast-container > .toast-success {
|
||||
background-image: url(../images/icon-success.svg) !important;
|
||||
background-position: top 20px left 20px;
|
||||
background-size: 40px 40px;
|
||||
background-position: top 12px left 12px;
|
||||
}
|
||||
|
||||
#toast-container > .toast-error {
|
||||
background-image: url(../images/icon-error.svg) !important;
|
||||
background-position: top 20px left 20px;
|
||||
background-size: 40px 40px;
|
||||
background-position: top 12px left 12px;
|
||||
}
|
||||
|
||||
#toast-container > .toast-warning {
|
||||
background-image: url(../images/icon-warning.svg) !important;
|
||||
background-size: 40px 40px;
|
||||
background-position: top 12px left 12px;
|
||||
}
|
||||
|
||||
.toast-success .toast-progress {
|
||||
@@ -216,7 +223,7 @@ input:checked + .slider:before {
|
||||
color: var(--ui-gray-7);
|
||||
background-color: var(--white-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px 20px 20px 80px;
|
||||
padding: 18px 20px 18px 68px;
|
||||
width: 300px;
|
||||
opacity: 1;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
@@ -232,6 +239,7 @@ input:checked + .slider:before {
|
||||
.toast-close-button {
|
||||
color: var(--black-color);
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
|
||||
@@ -249,8 +257,10 @@ input:checked + .slider:before {
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 500;
|
||||
color: var(--black-color);
|
||||
padding: 10px 0px;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
@@ -319,25 +329,6 @@ input:checked + .slider:before {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
/* Databatle Setting Menu */
|
||||
|
||||
.tableMenu {
|
||||
border: 1px solid var(--border-bootbox);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
[data-reach-menu-list],
|
||||
[data-reach-menu-items] {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dropdown-menu .tableMenu {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
/* Status Indicator Inside Table Section Label Style */
|
||||
.table .label {
|
||||
border-radius: 8px !important;
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
@apply !outline-none;
|
||||
@apply border border-solid border-transparent;
|
||||
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: inline-flex;
|
||||
.btn.disabled,
|
||||
.btn[disabled],
|
||||
fieldset[disabled] .btn {
|
||||
@apply opacity-40;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--text-button-hover-color);
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
@@ -15,53 +26,70 @@
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus,
|
||||
.btn-primary:active .active {
|
||||
background-color: var(--ui-blue-9);
|
||||
@apply text-white bg-blue-8 border-blue-8;
|
||||
@apply hover:text-white hover:bg-blue-9 hover:border-blue-9;
|
||||
@apply th-dark:hover:bg-blue-7 th-dark:hover:border-blue-7;
|
||||
}
|
||||
|
||||
.btn-primary:active,
|
||||
.btn-primary.active,
|
||||
.open > .dropdown-toggle.btn-primary {
|
||||
background-color: var(--ui-blue-9);
|
||||
@apply bg-blue-9 border-blue-5;
|
||||
}
|
||||
|
||||
.nav-pills > li.active > a,
|
||||
.nav-pills > li.active > a:hover,
|
||||
.nav-pills > li.active > a:focus {
|
||||
background-color: var(--ui-blue-8);
|
||||
@apply bg-blue-8;
|
||||
}
|
||||
|
||||
/* Button Secondary */
|
||||
.btn-secondary {
|
||||
@apply border border-solid;
|
||||
|
||||
@apply text-blue-9 bg-blue-2 border-blue-8;
|
||||
@apply hover:bg-blue-3;
|
||||
|
||||
@apply th-dark:text-blue-3 th-dark:bg-gray-10 th-dark:border-blue-7;
|
||||
@apply th-dark:hover:bg-blue-11;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--ui-error-8);
|
||||
@apply bg-error-8 border-error-8;
|
||||
@apply hover:bg-error-7 hover:border-error-7 hover:text-white;
|
||||
}
|
||||
|
||||
.btn-danger:active,
|
||||
.btn-danger.active,
|
||||
.open > .dropdown-toggle.btn-danger {
|
||||
@apply bg-error-8 text-white border-blue-5;
|
||||
}
|
||||
|
||||
.btn-dangerlight {
|
||||
@apply text-error-9 th-dark:text-white;
|
||||
@apply bg-error-3 th-dark:bg-error-9;
|
||||
@apply hover:bg-error-2 th-dark:hover:bg-error-11;
|
||||
@apply border-error-5 th-dark:border-error-7 th-highcontrast:border-error-7;
|
||||
@apply border border-solid;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--ui-success-7);
|
||||
}
|
||||
|
||||
.btn-dangerlight {
|
||||
border: 1px solid var(--text-button-dangerlight-color);
|
||||
color: var(--ui-error-9);
|
||||
}
|
||||
|
||||
.btn-dangerlight:hover {
|
||||
color: var(--ui-error-9) !important;
|
||||
background-color: var(--ui-error-2) !important;
|
||||
.btn-success:hover {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
/* secondary-grey */
|
||||
.btn-default,
|
||||
.btn-light {
|
||||
background-color: var(--bg-button-group-color);
|
||||
border: 1px solid var(--border-button-group);
|
||||
color: var(--text-button-group-color);
|
||||
}
|
||||
@apply bg-white border-gray-5 text-gray-9;
|
||||
@apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10;
|
||||
|
||||
.btn-light:hover {
|
||||
background-color: var(--ui-gray-2) !important;
|
||||
/* dark mode */
|
||||
@apply th-dark:bg-gray-warm-10 th-dark:border-gray-warm-7 th-dark:text-gray-warm-4;
|
||||
@apply th-dark:hover:bg-gray-warm-9 th-dark:hover:border-gray-6 th-dark:hover:text-gray-warm-4;
|
||||
}
|
||||
|
||||
.btn-light:active,
|
||||
@@ -70,40 +98,52 @@
|
||||
background-color: var(--ui-gray-3);
|
||||
}
|
||||
|
||||
/* Button Secondary */
|
||||
.btn-secondary {
|
||||
background-color: var(--ui-blue-2) !important;
|
||||
border: 1px solid var(--ui-blue-8) !important;
|
||||
color: var(--ui-blue-9) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus,
|
||||
.btn-secondary:active .active {
|
||||
background-color: var(--ui-blue-3) !important;
|
||||
color: var(--ui-blue-9) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
background-color: var(--ui-blue-1);
|
||||
border: 1px solid var(--ui-blue-1);
|
||||
color: var(--ui-blue-5);
|
||||
}
|
||||
|
||||
form a,
|
||||
.form-group a,
|
||||
.hyperlink {
|
||||
.hyperlink,
|
||||
.hyperlink:focus {
|
||||
color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
form a:hover,
|
||||
.form-group a:hover,
|
||||
.hyperlink:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--ui-blue-9);
|
||||
}
|
||||
|
||||
.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
|
||||
background: var(--bg-button-group-color) !important;
|
||||
color: var(--text-button-group-color);
|
||||
.btn-group {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.input-group-btn .btn.active,
|
||||
.btn-group .btn.active {
|
||||
@apply bg-blue-2 text-blue-10 border-blue-5;
|
||||
@apply th-dark:bg-blue-11 th-dark:text-blue-2 th-dark:border-blue-9;
|
||||
}
|
||||
|
||||
/* focus */
|
||||
|
||||
.btn-primary:focus,
|
||||
.btn-secondary:focus,
|
||||
.btn-light:focus {
|
||||
@apply border-blue-5;
|
||||
}
|
||||
|
||||
.btn-danger:focus,
|
||||
.btn-dangerlight:focus {
|
||||
@apply border-blue-6;
|
||||
}
|
||||
|
||||
.btn-primary:focus,
|
||||
.btn-secondary:focus,
|
||||
.btn-light:focus,
|
||||
.btn-danger:focus,
|
||||
.btn-dangerlight:focus {
|
||||
--btn-focus-color: var(--ui-blue-3);
|
||||
box-shadow: 0px 0px 0px 4px var(--btn-focus-color);
|
||||
}
|
||||
|
||||
[theme='dark'] .btn-primary:focus,
|
||||
[theme='dark'] .btn-secondary:focus,
|
||||
[theme='dark'] .btn-light:focus,
|
||||
[theme='dark'] .btn-danger:focus,
|
||||
[theme='dark'] .btn-dangerlight:focus {
|
||||
--btn-focus-color: var(--ui-blue-11);
|
||||
}
|
||||
|
||||
@@ -101,19 +101,6 @@ pr-icon {
|
||||
padding: 1.5%;
|
||||
}
|
||||
|
||||
.icon-nested-gray {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: var(--ui-gray-4);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.icon-nested-blue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -100,9 +100,7 @@ div.input-mask {
|
||||
line-height: 30px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.widget .widget-header i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.widget .widget-body {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
@@ -148,20 +146,7 @@ div.input-mask {
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px;
|
||||
}
|
||||
.widget .widget-icon {
|
||||
background: #30426a;
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.widget .widget-icon i {
|
||||
line-height: 66px;
|
||||
color: #ffffff;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.widget .widget-footer {
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px;
|
||||
|
||||
@@ -86,32 +86,31 @@
|
||||
|
||||
--orange-1: #e86925;
|
||||
|
||||
--BE-only: var(--orange-1);
|
||||
--BE-only: var(--ui-warning-7);
|
||||
|
||||
/* Default Theme */
|
||||
--bg-card-color: var(--white-color);
|
||||
--bg-main-color: var(--white-color);
|
||||
--bg-body-color: var(--grey-9);
|
||||
--bg-checkbox-border-color: var(--grey-49);
|
||||
--bg-sidebar-header-color: var(--grey-37);
|
||||
--bg-widget-color: var(--white-color);
|
||||
--bg-widget-header-color: var(--grey-10);
|
||||
--bg-widget-table-color: var(--grey-13);
|
||||
--bg-widget-table-color: var(--ui-gray-3);
|
||||
--bg-header-color: var(--white-color);
|
||||
--bg-hover-table-color: var(--grey-14);
|
||||
--bg-switch-box-color: var(--ui-gray-5);
|
||||
--bg-input-group-addon-color: var(--ui-gray-3);
|
||||
--bg-btn-default-color: var(--white-color);
|
||||
--bg-blocklist-hover-color: var(--ui-blue-3);
|
||||
--bg-boxselector-color: var(--white-color);
|
||||
--bg-blocklist-hover-color: var(--ui-blue-2);
|
||||
--bg-boxselector-color: var(--ui-gray-2);
|
||||
--bg-table-color: var(--white-color);
|
||||
--bg-md-checkbox-color: var(--grey-12);
|
||||
--bg-form-control-disabled-color: var(--grey-11);
|
||||
--bg-modal-content-color: var(--white-color);
|
||||
--bg-code-color: var(--grey-15);
|
||||
--bg-navtabs-color: var(--white-color);
|
||||
--bg-navtabs-hover-color: var(--grey-16);
|
||||
--bg-table-selected-color: var(--grey-14);
|
||||
--bg-codemirror-color: var(--white-color);
|
||||
--bg-codemirror-gutters-color: var(--grey-17);
|
||||
--bg-dropdown-menu-color: var(--white-color);
|
||||
--bg-log-viewer-color: var(--white-color);
|
||||
@@ -127,18 +126,11 @@
|
||||
--bg-motd-body-color: var(--grey-20);
|
||||
--bg-item-highlighted-color: var(--grey-21);
|
||||
--bg-item-highlighted-null-color: var(--grey-14);
|
||||
--bg-row-header-color: var(--white-color);
|
||||
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
|
||||
--bg-multiselect-checkbox-color: var(--white-color);
|
||||
--bg-panel-body-color: var(--white-color);
|
||||
--bg-codemirror-color: var(--white-color);
|
||||
--bg-codemirror-selected-color: var(--grey-22);
|
||||
--bg-tooltip-color: var(--ui-gray-11);
|
||||
--bg-input-sm-color: var(--white-color);
|
||||
--bg-service-datatable-thead: var(--grey-23);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
--bg-inner-datatable-thead: var(--grey-23);
|
||||
--bg-service-datatable-tbody: var(--grey-24);
|
||||
--bg-app-datatable-tbody: var(--grey-24);
|
||||
--bg-multiselect-color: var(--white-color);
|
||||
--bg-daterangepicker-color: var(--white-color);
|
||||
@@ -153,8 +145,6 @@
|
||||
--bg-btn-focus: var(--grey-59);
|
||||
--bg-boxselector-disabled-color: var(--white-color);
|
||||
--bg-small-select-color: var(--white-color);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
--bg-app-datatable-tbody: var(--grey-24);
|
||||
--bg-stepper-item-active: var(--white-color);
|
||||
--bg-stepper-item-counter: var(--grey-61);
|
||||
--bg-sortbutton-color: var(--white-color);
|
||||
@@ -165,6 +155,12 @@
|
||||
--bg-webeditor-color: var(--ui-gray-3);
|
||||
--bg-button-group-color: var(--ui-white);
|
||||
--bg-pagination-disabled-color: var(--ui-white);
|
||||
--bg-nav-container-color: var(--ui-gray-2);
|
||||
--bg-code-script-color: var(--ui-white);
|
||||
--bg-nav-tabs-active-color: var(--ui-gray-4);
|
||||
--bg-stepper-color: var(--ui-white);
|
||||
--bg-stepper-active-color: var(--ui-blue-1);
|
||||
--bg-code-color: var(--ui-white);
|
||||
|
||||
--text-main-color: var(--grey-7);
|
||||
--text-body-color: var(--grey-6);
|
||||
@@ -174,13 +170,11 @@
|
||||
--text-link-color: var(--blue-2);
|
||||
--text-link-hover-color: var(--blue-4);
|
||||
--text-input-group-addon-color: var(--grey-25);
|
||||
--text-btn-default-color: var(--grey-6);
|
||||
--text-blocklist-hover-color: var(--grey-37);
|
||||
--text-dashboard-item-color: var(--grey-32);
|
||||
--text-danger-color: var(--red-1);
|
||||
--text-code-color: var(--red-2);
|
||||
--text-code-color: var(--ui-gray-9);
|
||||
--text-navtabs-color: var(--grey-25);
|
||||
--text-form-section-title-color: var(--grey-26);
|
||||
--text-cm-default-color: var(--blue-1);
|
||||
--text-cm-meta-color: var(--black-color);
|
||||
--text-cm-string-color: var(--red-3);
|
||||
@@ -212,6 +206,8 @@
|
||||
--text-bootbox: var(--ui-gray-7);
|
||||
--text-button-group-color: var(--ui-gray-9);
|
||||
--text-button-dangerlight-color: var(--ui-error-5);
|
||||
--text-stepper-active-color: var(--ui-blue-8);
|
||||
--text-boxselector-header: var(--ui-black);
|
||||
|
||||
--border-color: var(--grey-42);
|
||||
--border-widget-color: var(--grey-43);
|
||||
@@ -226,29 +222,26 @@
|
||||
--border-boxselector-color: var(--grey-6);
|
||||
--border-md-checkbox-color: var(--grey-19);
|
||||
--border-modal-header-color: var(--grey-45);
|
||||
--border-navtabs-color: var(--grey-19);
|
||||
--border-form-section-title-color: var(--grey-26);
|
||||
--border-navtabs-color: var(--ui-white);
|
||||
--border-codemirror-cursor-color: var(--black-color);
|
||||
--border-codemirror-gutters-color: var(--grey-19);
|
||||
--border-pre-color: var(--grey-43);
|
||||
--border-blocklist-item-selected-color: var(--grey-46);
|
||||
--border-pagination-color: var(--ui-white);
|
||||
--border-pagination-span-color: var(--ui-white);
|
||||
--border-pagination-hover-color: var(--ui-white);
|
||||
--border-multiselect-button-color: var(--grey-48);
|
||||
--border-searchbar-color: var(--grey-10);
|
||||
--border-panel-color: var(--white-color);
|
||||
--border-input-sm-color: var(--grey-47);
|
||||
--border-daterangepicker-color: var(--grey-19);
|
||||
--border-calendar-table: var(--white-color);
|
||||
--border-daterangepicker: var(--grey-19);
|
||||
--border-pre-next-month: var(--black-color);
|
||||
--border-daterangepicker-after: var(--white-color);
|
||||
--border-tooltip-color: var(--grey-47);
|
||||
--border-modal: 0px;
|
||||
--border-sortbutton: var(--grey-8);
|
||||
--border-bootbox: var(--ui-gray-5);
|
||||
--border-blocklist: var(--ui-gray-5);
|
||||
--border-widget: var(--ui-gray-5);
|
||||
--border-nav-container-color: var(--ui-gray-5);
|
||||
--border-stepper-color: var(--ui-gray-4);
|
||||
|
||||
--shadow-box-color: 0 3px 10px -2px var(--grey-50);
|
||||
--shadow-boxselector-color: 0 3px 10px -2px var(--grey-50);
|
||||
@@ -256,7 +249,6 @@
|
||||
--button-close-color: var(--black-color);
|
||||
--button-opacity: 0.2;
|
||||
--button-opacity-hover: 0.5;
|
||||
--bg-boxselector-wrapper-color: var(--grey-6);
|
||||
|
||||
--bg-image-multiselect: linear-gradient(var(--blue-2), var(--blue-2));
|
||||
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
|
||||
@@ -281,37 +273,37 @@
|
||||
--text-button-group: var(--ui-gray-9);
|
||||
}
|
||||
|
||||
:root[theme='dark'] {
|
||||
--bg-card-color: var(--grey-1);
|
||||
--bg-main-color: var(--grey-2);
|
||||
/* Dark Theme */
|
||||
[theme='dark'] {
|
||||
--bg-body-color: var(--grey-2);
|
||||
--bg-btn-default-color: var(--grey-3);
|
||||
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
|
||||
--bg-boxselector-color: var(--ui-gray-iron-10);
|
||||
--bg-blocklist-item-selected-color: var(--grey-3);
|
||||
--bg-card-color: var(--grey-1);
|
||||
--bg-checkbox-border-color: var(--grey-8);
|
||||
--bg-widget-color: var(--grey-1);
|
||||
--bg-code-color: var(--ui-gray-warm-11);
|
||||
--bg-codemirror-color: var(--ui-gray-warm-11);
|
||||
--bg-codemirror-gutters-color: var(--ui-gray-warm-8);
|
||||
--bg-codemirror-selected-color: var(--ui-gray-warm-7);
|
||||
--bg-dropdown-menu-color: var(--ui-gray-7);
|
||||
--bg-main-color: var(--grey-2);
|
||||
--bg-widget-color: var(--ui-gray-warm-10);
|
||||
--bg-widget-header-color: var(--grey-1);
|
||||
--bg-widget-table-color: var(--grey-1);
|
||||
--bg-widget-table-color: var(--ui-gray-warm-9);
|
||||
--bg-header-color: var(--grey-2);
|
||||
--bg-hover-table-color: var(--grey-3);
|
||||
--bg-switch-box-color: var(--grey-53);
|
||||
--bg-input-group-addon-color: var(--grey-3);
|
||||
--bg-btn-default-color: var(--grey-3);
|
||||
--bg-blocklist-hover-color: var(--grey-3);
|
||||
--bg-boxselector-color: var(--grey-54);
|
||||
--bg-table-color: var(--grey-1);
|
||||
--bg-md-checkbox-color: var(--grey-31);
|
||||
--bg-form-control-disabled-color: var(--grey-3);
|
||||
--bg-modal-content-color: var(--grey-1);
|
||||
--bg-code-color: var(--red-4);
|
||||
--bg-navtabs-color: var(--grey-3);
|
||||
--bg-navtabs-color: var(--ui-gray-warm-11);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-table-selected-color: var(--grey-3);
|
||||
--bg-codemirror-color: var(--ui-gray-warm-11);
|
||||
--bg-codemirror-gutters-color: var(--ui-gray-warm-8);
|
||||
--bg-codemirror-selected-color: var(--ui-gray-warm-7);
|
||||
--bg-dropdown-menu-color: var(--grey-1);
|
||||
--bg-log-viewer-color: var(--grey-2);
|
||||
--bg-log-line-selected-color: var(--grey-3);
|
||||
--bg-pre-color: var(--grey-2);
|
||||
--bg-blocklist-item-selected-color: var(--grey-3);
|
||||
--bg-progress-color: var(--grey-3);
|
||||
--bg-pagination-color: var(--grey-3);
|
||||
--bg-pagination-span-color: var(--grey-1);
|
||||
@@ -320,39 +312,43 @@
|
||||
--bg-motd-body-color: var(--grey-1);
|
||||
--bg-item-highlighted-color: var(--grey-2);
|
||||
--bg-item-highlighted-null-color: var(--grey-2);
|
||||
--bg-row-header-color: var(--grey-2);
|
||||
--bg-multiselect-button-color: var(--grey-3);
|
||||
--bg-image-multiselect-button: none !important;
|
||||
--bg-multiselect-checkbox-color: var(--grey-3);
|
||||
--bg-panel-body-color: var(--grey-1);
|
||||
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
|
||||
--bg-sidebar-header-color: var(--grey-1);
|
||||
--bg-input-group-addon-color: var(--grey-3);
|
||||
--bg-tooltip-color: var(--grey-3);
|
||||
--bg-input-sm-color: var(--grey-1);
|
||||
--bg-service-datatable-thead: var(--grey-1);
|
||||
--bg-inner-datatable-thead: var(--grey-1);
|
||||
--bg-app-datatable-thead: var(--grey-1);
|
||||
--bg-service-datatable-tbody: var(--grey-1);
|
||||
--bg-app-datatable-tbody: var(--grey-1);
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
--bg-daterangepicker-color: var(--grey-3);
|
||||
--bg-calendar-color: var(--grey-3);
|
||||
--bg-calendar-table-color: var(--grey-3);
|
||||
--bg-daterangepicker-end-date: var(--grey-4);
|
||||
--bg-daterangepicker-hover: var(--grey-4);
|
||||
--bg-daterangepicker-in-range: var(--grey-2);
|
||||
--bg-daterangepicker-in-range: var(--ui-gray-warm-11);
|
||||
--bg-daterangepicker-active: var(--blue-14);
|
||||
--bg-tooltip-color: var(--grey-3);
|
||||
--bg-input-autofill-color: var(--grey-2);
|
||||
--bg-btn-default-hover-color: var(--grey-3);
|
||||
--bg-btn-focus: var(--grey-3);
|
||||
--bg-boxselector-disabled-color: var(--grey-54);
|
||||
--bg-small-select-color: var(--grey-2);
|
||||
--bg-app-datatable-thead: var(--grey-1);
|
||||
--bg-app-datatable-tbody: var(--grey-1);
|
||||
--bg-stepper-item-active: var(--grey-1);
|
||||
--bg-stepper-item-counter: var(--grey-7);
|
||||
--bg-sortbutton-color: var(--grey-1);
|
||||
--bg-dashboard-item: var(--grey-3);
|
||||
--bg-searchbar: var(--grey-1);
|
||||
--bg-searchbar: var(--ui-grey-warm-11);
|
||||
--bg-inputbox: var(--grey-2);
|
||||
--bg-dropdown-hover: var(--grey-3);
|
||||
--bg-webeditor-color: var(--ui-gray-warm-9);
|
||||
--bg-button-group-color: var(--ui-black);
|
||||
--bg-pagination-disabled-color: var(--grey-1);
|
||||
--bg-nav-container-color: var(--ui-gray-iron-10);
|
||||
--bg-code-script-color: var(--ui-gray-warm-11);
|
||||
--bg-nav-tabs-active-color: var(--ui-gray-warm-9);
|
||||
--bg-stepper-color: var(--ui-gray-iron-10);
|
||||
--bg-stepper-active-color: var(--ui-blue-8);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
@@ -362,13 +358,11 @@
|
||||
--text-link-color: var(--blue-9);
|
||||
--text-link-hover-color: var(--blue-2);
|
||||
--text-input-group-addon-color: var(--grey-8);
|
||||
--text-btn-default-color: var(--grey-8);
|
||||
--text-blocklist-hover-color: var(--white-color);
|
||||
--text-dashboard-item-color: var(--blue-2);
|
||||
--text-danger-color: var(--red-4);
|
||||
--text-code-color: var(--white-color);
|
||||
--text-navtabs-color: var(--white-color);
|
||||
--text-form-section-title-color: var(--grey-8);
|
||||
--text-cm-default-color: var(--blue-10);
|
||||
--text-cm-meta-color: var(--white-color);
|
||||
--text-cm-string-color: var(--red-5);
|
||||
@@ -388,20 +382,21 @@
|
||||
--text-ui-select-color: var(--white-color);
|
||||
--text-ui-select-hover-color: var(--white-color);
|
||||
--text-summary-color: var(--white-color);
|
||||
--text-multiselect-button-color: var(--white-color);
|
||||
--text-multiselect-item-color: var(--white-color);
|
||||
--text-boxselector-wrapper-color: var(--white-color);
|
||||
--text-tooltip-color: var(--white-color);
|
||||
--text-rzslider-color: var(--white-color);
|
||||
--text-rzslider-limit-color: var(--white-color);
|
||||
--text-daterangepicker-end-date: var(--grey-7);
|
||||
--text-daterangepicker-in-range: var(--white-color);
|
||||
--text-daterangepicker-active: var(--white-color);
|
||||
--text-tooltip-color: var(--white-color);
|
||||
--text-btn-default-color: var(--white-color);
|
||||
--text-input-autofill-color: var(--grey-8);
|
||||
--text-button-hover-color: var(--white-color);
|
||||
--text-small-select-color: var(--grey-7);
|
||||
--text-bootbox: var(--white-color);
|
||||
--text-button-group-color: var(--ui-white);
|
||||
--text-button-dangerlight-color: var(--ui-error-7);
|
||||
--text-stepper-active-color: var(--ui-white);
|
||||
--text-boxselector-header: var(--ui-white);
|
||||
|
||||
--border-color: var(--grey-3);
|
||||
--border-widget-color: var(--grey-1);
|
||||
@@ -417,27 +412,26 @@
|
||||
--border-md-checkbox-color: var(--grey-41);
|
||||
--border-modal-header-color: var(--grey-1);
|
||||
--border-navtabs-color: var(--grey-38);
|
||||
--border-form-section-title-color: var(--grey-8);
|
||||
--border-codemirror-cursor-color: var(--white-color);
|
||||
--border-codemirror-gutters-color: var(--grey-26);
|
||||
--border-pre-color: var(--grey-3);
|
||||
--border-blocklist-item-selected-color: var(--grey-38);
|
||||
--border-pagination-span-color: var(--grey-1);
|
||||
--border-pagination-hover-color: var(--grey-3);
|
||||
--border-boxselector-wrapper-hover: 3px solid var(--blue-8);
|
||||
--border-panel-color: var(--grey-2);
|
||||
--border-input-sm-color: var(--grey-3);
|
||||
--border-daterangepicker-color: var(--grey-3);
|
||||
--border-calendar-table: var(--grey-3);
|
||||
--border-daterangepicker: var(--grey-4);
|
||||
--border-pre-next-month: var(--white-color);
|
||||
--border-daterangepicker-after: var(--grey-3);
|
||||
--border-tooltip-color: var(--grey-3);
|
||||
--border-modal: 0px;
|
||||
--border-sortbutton: var(--grey-3);
|
||||
--border-bootbox: var(--ui-gray-9);
|
||||
--border-blocklist: var(--ui-gray-9);
|
||||
--border-widget: var(--ui-gray-9);
|
||||
--border-pagination-color: var(--grey-1);
|
||||
--border-nav-container-color: var(--ui-gray-neutral-8);
|
||||
--border-stepper-color: var(--ui-gray-warm-9);
|
||||
|
||||
--blue-color: var(--blue-2);
|
||||
--button-close-color: var(--white-color);
|
||||
@@ -462,13 +456,14 @@
|
||||
--sort-icon: var(--ui-gray-3);
|
||||
--border-checkbox: var(--ui-gray-5);
|
||||
--bg-checkbox: var(--white-color);
|
||||
--border-searchbar: var(--ui-gray-5);
|
||||
--border-searchbar: var(--ui-gray-warm-9);
|
||||
--bg-button-group: var(--white-color);
|
||||
--border-button-group: var(--ui-gray-5);
|
||||
--text-button-group: var(--ui-gray-9);
|
||||
}
|
||||
|
||||
:root[theme='highcontrast'] {
|
||||
/* High Contrast Theme */
|
||||
[theme='highcontrast'] {
|
||||
--bg-card-color: var(--black-color);
|
||||
--bg-main-color: var(--black-color);
|
||||
--bg-body-color: var(--black-color);
|
||||
@@ -480,10 +475,8 @@
|
||||
--bg-hover-table-color: var(--grey-3);
|
||||
--bg-switch-box-color: var(--grey-53);
|
||||
--bg-panel-body-color: var(--black-color);
|
||||
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
|
||||
--bg-dropdown-menu-color: var(--black-color);
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-row-header-color: var(--black-color);
|
||||
--bg-motd-body-color: var(--black-color);
|
||||
--bg-blocklist-hover-color: var(--black-color);
|
||||
--bg-blocklist-item-selected-color: var(--black-color);
|
||||
@@ -494,14 +487,15 @@
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-log-viewer-color: var(--black-color);
|
||||
--bg-log-line-selected-color: var(--grey-3);
|
||||
--bg-sidebar-header-color: var(--black-color);
|
||||
--bg-modal-content-color: var(--black-color);
|
||||
--bg-form-control-disabled-color: var(--grey-1);
|
||||
--bg-input-sm-color: var(--black-color);
|
||||
--bg-item-highlighted-color: var(--black-color);
|
||||
--bg-service-datatable-thead: var(--black-color);
|
||||
--bg-inner-datatable-thead: var(--black-color);
|
||||
--bg-app-datatable-thead: var(--black-color);
|
||||
--bg-service-datatable-tbody: var(--black-color);
|
||||
--bg-app-datatable-tbody: var(--black-color);
|
||||
--bg-pagination-color: var(--grey-3);
|
||||
--bg-pagination-span-color: var(--ui-black);
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
@@ -517,11 +511,9 @@
|
||||
--bg-pre-color: var(--grey-2);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-btn-default-color: var(--black-color);
|
||||
--bg-code-color: var(--red-4);
|
||||
--bg-navtabs-color: var(--black-color);
|
||||
--bg-input-autofill-color: var(--black-color);
|
||||
--bg-code-color: var(--grey-2);
|
||||
--bg-navtabs-color: var(--grey-2);
|
||||
--bg-code-color: var(--ui-black);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-btn-default-hover-color: var(--grey-3);
|
||||
--bg-btn-default-color: var(--black-color);
|
||||
@@ -529,8 +521,6 @@
|
||||
--bg-boxselector-color: var(--black-color);
|
||||
--bg-boxselector-disabled-color: var(--black-color);
|
||||
--bg-small-select-color: var(--black-color);
|
||||
--bg-app-datatable-thead: var(--black-color);
|
||||
--bg-app-datatable-tbody: var(--black-color);
|
||||
--bg-stepper-item-active: var(--black-color);
|
||||
--bg-stepper-item-counter: var(--grey-3);
|
||||
--bg-sortbutton-color: var(--grey-1);
|
||||
@@ -540,6 +530,11 @@
|
||||
--bg-webeditor-color: var(--ui-gray-warm-9);
|
||||
--bg-pagination-disabled-color: var(--ui-black);
|
||||
--bg-pagination-hover-color: var(--ui-black);
|
||||
--bg-nav-container-color: var(--ui-black);
|
||||
--bg-code-script-color: var(--ui-black);
|
||||
--bg-nav-tabs-active-color: var(--ui-black);
|
||||
--bg-stepper-active-color: var(--ui-blue-8);
|
||||
--bg-stepper-color: var(--ui-black);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
@@ -552,7 +547,6 @@
|
||||
--text-blocklist-hover-color: var(--blue-11);
|
||||
--text-boxselector-wrapper-color: var(--white-color);
|
||||
--text-dashboard-item-color: var(--blue-12);
|
||||
--text-form-section-title-color: var(--white-color);
|
||||
--text-muted-color: var(--white-color);
|
||||
--text-tooltip-color: var(--white-color);
|
||||
--text-blocklist-item-selected-color: var(--blue-9);
|
||||
@@ -564,11 +558,10 @@
|
||||
--text-rzslider-color: var(--white-color);
|
||||
--text-rzslider-limit-color: var(--white-color);
|
||||
--text-pagination-color: var(--white-color);
|
||||
--text-daterangepicker-end-date: var(--grey-7);
|
||||
--text-daterangepicker-end-date: var(--ui-white);
|
||||
--text-daterangepicker-in-range: var(--white-color);
|
||||
--text-daterangepicker-active: var(--white-color);
|
||||
--text-ui-select-color: var(--white-color);
|
||||
--text-btn-default-color: var(--white-color);
|
||||
--text-json-tree-color: var(--white-color);
|
||||
--text-json-tree-leaf-color: var(--white-color);
|
||||
--text-json-tree-branch-preview-color: var(--white-color);
|
||||
@@ -577,13 +570,12 @@
|
||||
--text-input-autofill-color: var(--white-color);
|
||||
--text-navtabs-color: var(--white-color);
|
||||
--text-button-hover-color: var(--white-color);
|
||||
--text-btn-default-color: var(--white-color);
|
||||
--text-small-select-color: var(--white-color);
|
||||
--text-multiselect-item-color: var(--white-color);
|
||||
--text-pagination-span-color: var(--ui-white);
|
||||
--text-bootbox: var(--white-color);
|
||||
--text-button-dangerlight-color: var(--ui-error-7);
|
||||
--text-pagination-span-hover-color: var(--ui-white);
|
||||
--text-stepper-active-color: var(--ui-white);
|
||||
--text-boxselector-header: var(--ui-white);
|
||||
|
||||
--border-color: var(--grey-55);
|
||||
--border-widget-color: var(--white-color);
|
||||
@@ -594,8 +586,6 @@
|
||||
--border-datatable-top-color: var(--grey-55);
|
||||
--border-sidebar-high-contrast: 1px solid var(--blue-9);
|
||||
--border-code-high-contrast: 1px solid var(--white-color);
|
||||
--border-boxselector-wrapper: 3px solid var(--blue-2);
|
||||
--border-boxselector-wrapper-hover: 3px solid var(--blue-8);
|
||||
--border-panel-color: var(--white-color);
|
||||
--border-input-group-addon-color: var(--grey-54);
|
||||
--border-modal-header-color: var(--grey-3);
|
||||
@@ -607,7 +597,6 @@
|
||||
--border-daterangepicker: var(--black-color);
|
||||
--border-pre-next-month: var(--white-color);
|
||||
--border-daterangepicker-after: var(--black-color);
|
||||
--border-tooltip-color: var(--white-color);
|
||||
--border-pre-color: var(--grey-3);
|
||||
--border-codemirror-cursor-color: var(--white-color);
|
||||
--border-modal: 1px solid var(--white-color);
|
||||
@@ -616,6 +605,8 @@
|
||||
--border-bootbox: var(--black-color);
|
||||
--border-blocklist: var(--white-color);
|
||||
--border-widget: var(--white-color);
|
||||
--border-nav-container-color: var(--ui-white);
|
||||
--border-stepper-color: var(--ui-gray-warm-9);
|
||||
|
||||
--shadow-box-color: none;
|
||||
--shadow-boxselector-color: none;
|
||||
|
||||
@@ -48,18 +48,12 @@ a:focus {
|
||||
border: 1px solid var(--border-input-group-addon-color);
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
color: var(--text-btn-default-color);
|
||||
background-color: var(--bg-btn-default-color);
|
||||
border-color: var(--border-btn-default-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--ui-error-9);
|
||||
}
|
||||
|
||||
.table .table {
|
||||
background-color: var(--bg-table-color);
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
@@ -207,6 +201,7 @@ code {
|
||||
|
||||
.dropdown-menu {
|
||||
background: var(--bg-dropdown-menu-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a {
|
||||
@@ -215,6 +210,7 @@ code {
|
||||
|
||||
pre {
|
||||
border: 1px solid var(--border-pre-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-pre-color);
|
||||
color: var(--text-pre-color);
|
||||
}
|
||||
@@ -318,7 +314,6 @@ json-tree .branch-preview {
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {
|
||||
background: var(--text-input-textarea);
|
||||
@@ -398,33 +393,10 @@ input:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--text-input-autofill-color) !important;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--text-button-hover-color);
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: var(--bg-btn-default-hover-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: var(--white-color) !important;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
/* Overide Vendor CSS */
|
||||
|
||||
.btn.disabled,
|
||||
.btn[disabled],
|
||||
fieldset[disabled] .btn {
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
.btn-link:hover {
|
||||
color: var(--text-link-hover-color) !important;
|
||||
}
|
||||
|
||||
.multiSelect.inlineBlock button {
|
||||
@@ -432,12 +404,49 @@ fieldset[disabled] .btn {
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > a {
|
||||
border-top: 0px;
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
border-bottom: 3px solid red;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.label-default {
|
||||
line-height: 11px;
|
||||
}
|
||||
|
||||
/* Code Script Style */
|
||||
.code-script {
|
||||
background-color: var(--bg-code-script-color);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
border: 1px solid var(--border-nav-container-color);
|
||||
background-color: var(--bg-nav-container-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
background-color: var(--bg-nav-tabs-active-color);
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Code Script Style */
|
||||
.code-script {
|
||||
background-color: var(--bg-code-script-color);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
border: 1px solid var(--border-nav-container-color);
|
||||
background-color: var(--bg-nav-container-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
background-color: var(--bg-nav-tabs-active-color);
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
11
app/assets/ico/agent.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9538_418895)">
|
||||
<path d="M15.0049 13.2509L8.75488 20.7509H14.3799L13.7549 25.7509L20.0049 18.2509H14.3799L15.0049 13.2509Z" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9538_418895">
|
||||
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 719 B |
4
app/assets/ico/api.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path d="M9.32758 23.1544V23.0281C9.32758 21.9669 9.32758 21.4364 9.53409 21.0311C9.71574 20.6746 10.0056 20.3847 10.3621 20.2031C10.7674 19.9966 11.298 19.9966 12.3591 19.9966H16.4011C17.4622 19.9966 17.9928 19.9966 18.3981 20.2031C18.7546 20.3847 19.0444 20.6746 19.2261 21.0311C19.4326 21.4364 19.4326 21.9669 19.4326 23.0281V23.1544M9.32758 23.1544C8.62997 23.1544 8.06445 23.7199 8.06445 24.4175C8.06445 25.1151 8.62997 25.6806 9.32758 25.6806C10.0252 25.6806 10.5907 25.1151 10.5907 24.4175C10.5907 23.7199 10.0252 23.1544 9.32758 23.1544ZM19.4326 23.1544C18.735 23.1544 18.1695 23.7199 18.1695 24.4175C18.1695 25.1151 18.735 25.6806 19.4326 25.6806C20.1302 25.6806 20.6957 25.1151 20.6957 24.4175C20.6957 23.7199 20.1302 23.1544 19.4326 23.1544ZM14.3801 23.1544C13.6825 23.1544 13.117 23.7199 13.117 24.4175C13.117 25.1151 13.6825 25.6806 14.3801 25.6806C15.0777 25.6806 15.6432 25.1151 15.6432 24.4175C15.6432 23.7199 15.0777 23.1544 14.3801 23.1544ZM14.3801 23.1544V16.8388M10.5907 16.8388H18.1695C18.758 16.8388 19.0523 16.8388 19.2844 16.7426C19.5939 16.6144 19.8398 16.3685 19.968 16.059C20.0641 15.8269 20.0641 15.5326 20.0641 14.9441C20.0641 14.3555 20.0641 14.0613 19.968 13.8291C19.8398 13.5196 19.5939 13.2737 19.2844 13.1455C19.0523 13.0494 18.758 13.0494 18.1695 13.0494H10.5907C10.0022 13.0494 9.70789 13.0494 9.47576 13.1455C9.16626 13.2737 8.92036 13.5196 8.79217 13.8291C8.69602 14.0613 8.69602 14.3555 8.69602 14.9441C8.69602 15.5326 8.69602 15.8269 8.79217 16.059C8.92036 16.3685 9.16626 16.6144 9.47576 16.7426C9.70789 16.8388 10.0022 16.8388 10.5907 16.8388Z" stroke="#0086C9" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
app/assets/ico/cloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
11
app/assets/ico/edge-agent.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9538_418898)">
|
||||
<path d="M18.1297 18.2509H17.3422C17.1084 17.3452 16.6252 16.5233 15.9476 15.8786C15.27 15.2339 14.4252 14.7921 13.509 14.6035C12.5929 14.415 11.6423 14.4871 10.7651 14.8118C9.88797 15.1366 9.11948 15.7008 8.54699 16.4405C7.9745 17.1801 7.62095 18.0655 7.52652 18.9961C7.4321 19.9266 7.60058 20.865 8.01282 21.7046C8.42506 22.5442 9.06453 23.2513 9.85857 23.7456C10.6526 24.2399 11.5694 24.5016 12.5047 24.5009H18.1297C18.9585 24.5009 19.7534 24.1716 20.3394 23.5856C20.9255 22.9995 21.2547 22.2047 21.2547 21.3759C21.2547 20.5471 20.9255 19.7522 20.3394 19.1661C19.7534 18.5801 18.9585 18.2509 18.1297 18.2509Z" stroke="#0086C9" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9538_418898">
|
||||
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg><svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6665 33.3338 65 33.3338H82.2106M58.3333 72.9168L68.75 62.5002L58.3333 52.0835M41.6667 52.0835L31.25 62.5002L41.6667 72.9168M83.3333 41.6178V71.6668C83.3333 78.6675 83.3333 82.1678 81.9709 84.8417C80.7725 87.1937 78.8602 89.106 76.5082 90.3044C73.8343 91.6668 70.334 91.6668 63.3333 91.6668H36.6667C29.666 91.6668 26.1657 91.6668 23.4918 90.3044C21.1398 89.106 19.2275 87.1937 18.0291 84.8417C16.6667 82.1678 16.6667 78.6675 16.6667 71.6668V28.3335C16.6667 21.3328 16.6667 17.8325 18.0291 15.1586C19.2275 12.8066 21.1398 10.8943 23.4918 9.69591C26.1657 8.3335 29.666 8.3335 36.6667 8.3335H50.0491C53.1064 8.3335 54.6351 8.3335 56.0737 8.67887C57.3492 8.98508 58.5685 9.49014 59.6869 10.1755C60.9484 10.9485 62.0293 12.0295 64.1912 14.1914L77.4755 27.4756C79.6374 29.6375 80.7183 30.7185 81.4913 31.9799C82.1767 33.0983 82.6818 34.3176 82.988 35.5931C83.3333 37.0317 83.3333 38.5604 83.3333 41.6178Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 1.6 KiB |
11
app/assets/ico/import.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="37" height="40" viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8817 5.28003C22.6592 5.28003 28.9649 11.5865 28.9649 19.365C28.9649 27.1435 22.6592 33.45 14.8817 33.45C7.10065 33.4535 0.794922 27.1435 0.794922 19.365C0.794922 11.5865 7.10065 5.28003 14.8817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9540_418847)">
|
||||
<path d="M17.3803 22.0008L14.8803 19.5008M14.8803 19.5008L12.3803 22.0008M14.8803 19.5008V25.1258M20.124 23.4946C20.7336 23.1623 21.2152 22.6364 21.4927 22C21.7702 21.3636 21.8279 20.6529 21.6567 19.98C21.4854 19.3072 21.095 18.7105 20.547 18.2842C19.9989 17.858 19.3246 17.6263 18.6303 17.6258H17.8428C17.6536 16.8941 17.301 16.2148 16.8115 15.639C16.322 15.0631 15.7083 14.6058 15.0166 14.3012C14.3249 13.9967 13.5731 13.8529 12.8179 13.8808C12.0626 13.9086 11.3235 14.1073 10.656 14.4619C9.98859 14.8165 9.41023 15.3178 8.96442 15.9281C8.51862 16.5384 8.21697 17.2418 8.08215 17.9855C7.94733 18.7291 7.98285 19.4937 8.18604 20.2216C8.38924 20.9496 8.75481 21.622 9.25529 22.1883" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9540_418847">
|
||||
<rect width="15" height="15" fill="white" transform="translate(7.37988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
4
app/assets/ico/lock.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="76" height="75" viewBox="0 0 76 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.2" x="0.5" width="75" height="75" rx="37.5" fill="#713B12"/>
|
||||
<path d="M31.5353 36.1666V30.8333C31.5353 29.0652 32.2164 27.3695 33.4288 26.1192C34.6412 24.869 36.2855 24.1666 38 24.1666C39.7145 24.1666 41.3588 24.869 42.5712 26.1192C43.7835 27.3695 44.4646 29.0652 44.4646 30.8333V36.1666M28.9495 36.1666H47.0505C48.4786 36.1666 49.6364 37.3605 49.6364 38.8333V48.1666C49.6364 49.6394 48.4786 50.8333 47.0505 50.8333H28.9495C27.5214 50.8333 26.3636 49.6394 26.3636 48.1666V38.8333C26.3636 37.3605 27.5214 36.1666 28.9495 36.1666Z" stroke="#FEC84B" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 735 B |
35
app/assets/ico/logomark.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg width="38" height="47" viewBox="0 0 38 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd_1083_50505)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0154 11.0576H14.4116V14.1824H15.0154V11.0576Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6118 11.0576H17.008V14.1824H17.6118V11.0576Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.1593 5.09492L20.5404 4.02313L10.215 9.98588L10.834 11.0577L21.1593 5.09492Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.0989 5.09492L21.7178 4.02313L32.0432 9.98588L31.4243 11.0577L21.0989 5.09492Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8698 11.0727V9.8349H5.5807V11.0727H33.8698Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.6688 28.5534V10.2123H23.9067V29.444C23.5746 29.0666 23.1519 28.7949 22.6688 28.5534Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5555 28.2364V2.36261H21.7933V28.3873C21.4461 28.2213 20.6008 28.2364 20.5555 28.2364Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.82625 30.8177C7.31669 29.7006 6.32039 27.9193 6.32039 25.8965C6.32039 24.8247 6.6072 23.7681 7.13555 22.8472H17.7024C18.2459 23.7681 18.5176 24.8247 18.5176 25.8965C18.5176 26.8325 18.3968 27.708 18.0194 28.493C17.2194 27.7231 16.0419 27.391 14.8494 27.391C12.736 27.391 10.9245 28.7043 10.4566 30.6667C10.2905 30.6516 10.1848 30.6365 10.0188 30.6365C9.61122 30.6516 9.21873 30.712 8.82625 30.8177Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0191 15.5712H10.8188V18.7865H14.0191V15.5712Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4113 15.5712H7.21101V18.7865H10.4113V15.5712Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4113 19.1489H7.21101V22.3642H10.4113V19.1489Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0191 19.1489H10.8188V22.3642H14.0191V19.1489Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6118 19.1489H14.4116V22.3642H17.6118V19.1489Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6118 13.8503H14.4116V17.0657H17.6118V13.8503Z" fill="#13BEF9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9849 31.3007C11.4227 29.444 13.0983 28.0552 15.0909 28.0552C16.374 28.0552 17.5213 28.6288 18.3062 29.5345C18.9855 29.0666 19.8007 28.7949 20.6913 28.7949C23.016 28.7949 24.903 30.6818 24.903 33.0065C24.903 33.4896 24.8275 33.9424 24.6766 34.3802C25.1898 35.0746 25.5068 35.9501 25.5068 36.8861C25.5068 39.2108 23.6199 41.0977 21.2952 41.0977C20.2687 41.0977 19.3327 40.7354 18.6081 40.1316C17.8383 41.2034 16.5853 41.9129 15.1664 41.9129C13.536 41.9129 12.1171 40.977 11.4076 39.6184C11.1208 39.6787 10.8339 39.7089 10.532 39.7089C8.20731 39.7089 6.30527 37.822 6.30527 35.4973C6.30527 33.1726 8.19222 31.2856 10.532 31.2856C10.683 31.2705 10.8339 31.2705 10.9849 31.3007Z" fill="#13BEF9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_1083_50505" x="0" y="0" width="38" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.06 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1083_50505"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1083_50505" result="effect2_dropShadow_1083_50505"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1083_50505" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
4
app/assets/ico/socket.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path d="M14.3797 23.5025C16.98 23.5025 19.0879 21.3946 19.0879 18.7943L19.0879 16.3519L16.7331 16.3519M14.3797 23.5025C11.7795 23.5025 9.67156 21.3946 9.67156 18.7943L9.67156 16.3519L16.7331 16.3519M14.3797 23.5025C14.3797 24.9593 14.3797 26.5726 16.7331 26.5726L17.7794 26.5726M16.7331 16.3519L16.7331 13.4274M12.0216 16.3519L12.0216 13.4274" stroke="#0086C9" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 768 B |
1
app/assets/ico/upload-cloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 435 B |
1
app/assets/ico/vendor/internal.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M14.3805 14.085C11.3543 14.085 8.9012 16.5097 8.9012 19.5009C8.9012 22.492 11.3543 24.9168 14.3805 24.9168C17.4066 24.9168 19.8597 22.492 19.8597 19.5009C19.8597 16.5097 17.4066 14.085 14.3805 14.085ZM7.81738 19.5009C7.81738 15.9181 10.7558 13.0137 14.3805 13.0137C18.0052 13.0137 20.9436 15.9181 20.9436 19.5009C20.9436 23.0837 18.0052 25.9881 14.3805 25.9881C10.7558 25.9881 7.81738 23.0837 7.81738 19.5009ZM14.3805 16.5846C14.6798 16.5846 14.9224 16.8244 14.9224 17.1203V20.5884L16.4058 19.1221C16.6174 18.9129 16.9605 18.9129 17.1721 19.1221C17.3838 19.3313 17.3838 19.6705 17.1721 19.8796L14.7637 22.2603C14.552 22.4694 14.2089 22.4694 13.9973 22.2603L11.5888 19.8796C11.3772 19.6705 11.3772 19.3313 11.5888 19.1221C11.8004 18.9129 12.1436 18.9129 12.3552 19.1221L13.8386 20.5884V17.1203C13.8386 16.8244 14.0812 16.5846 14.3805 16.5846Z" fill="#0086C9"/> </svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
11
app/assets/ico/wizard/agent.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9538_418895)">
|
||||
<path d="M15.0049 13.2509L8.75488 20.7509H14.3799L13.7549 25.7509L20.0049 18.2509H14.3799L15.0049 13.2509Z" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9538_418895">
|
||||
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 719 B |
4
app/assets/ico/wizard/api.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path d="M9.32758 23.1544V23.0281C9.32758 21.9669 9.32758 21.4364 9.53409 21.0311C9.71574 20.6746 10.0056 20.3847 10.3621 20.2031C10.7674 19.9966 11.298 19.9966 12.3591 19.9966H16.4011C17.4622 19.9966 17.9928 19.9966 18.3981 20.2031C18.7546 20.3847 19.0444 20.6746 19.2261 21.0311C19.4326 21.4364 19.4326 21.9669 19.4326 23.0281V23.1544M9.32758 23.1544C8.62997 23.1544 8.06445 23.7199 8.06445 24.4175C8.06445 25.1151 8.62997 25.6806 9.32758 25.6806C10.0252 25.6806 10.5907 25.1151 10.5907 24.4175C10.5907 23.7199 10.0252 23.1544 9.32758 23.1544ZM19.4326 23.1544C18.735 23.1544 18.1695 23.7199 18.1695 24.4175C18.1695 25.1151 18.735 25.6806 19.4326 25.6806C20.1302 25.6806 20.6957 25.1151 20.6957 24.4175C20.6957 23.7199 20.1302 23.1544 19.4326 23.1544ZM14.3801 23.1544C13.6825 23.1544 13.117 23.7199 13.117 24.4175C13.117 25.1151 13.6825 25.6806 14.3801 25.6806C15.0777 25.6806 15.6432 25.1151 15.6432 24.4175C15.6432 23.7199 15.0777 23.1544 14.3801 23.1544ZM14.3801 23.1544V16.8388M10.5907 16.8388H18.1695C18.758 16.8388 19.0523 16.8388 19.2844 16.7426C19.5939 16.6144 19.8398 16.3685 19.968 16.059C20.0641 15.8269 20.0641 15.5326 20.0641 14.9441C20.0641 14.3555 20.0641 14.0613 19.968 13.8291C19.8398 13.5196 19.5939 13.2737 19.2844 13.1455C19.0523 13.0494 18.758 13.0494 18.1695 13.0494H10.5907C10.0022 13.0494 9.70789 13.0494 9.47576 13.1455C9.16626 13.2737 8.92036 13.5196 8.79217 13.8291C8.69602 14.0613 8.69602 14.3555 8.69602 14.9441C8.69602 15.5326 8.69602 15.8269 8.79217 16.059C8.92036 16.3685 9.16626 16.6144 9.47576 16.7426C9.70789 16.8388 10.0022 16.8388 10.5907 16.8388Z" stroke="#0086C9" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
11
app/assets/ico/wizard/edge-agent.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9538_418898)">
|
||||
<path d="M18.1297 18.2509H17.3422C17.1084 17.3452 16.6252 16.5233 15.9476 15.8786C15.27 15.2339 14.4252 14.7921 13.509 14.6035C12.5929 14.415 11.6423 14.4871 10.7651 14.8118C9.88797 15.1366 9.11948 15.7008 8.54699 16.4405C7.9745 17.1801 7.62095 18.0655 7.52652 18.9961C7.4321 19.9266 7.60058 20.865 8.01282 21.7046C8.42506 22.5442 9.06453 23.2513 9.85857 23.7456C10.6526 24.2399 11.5694 24.5016 12.5047 24.5009H18.1297C18.9585 24.5009 19.7534 24.1716 20.3394 23.5856C20.9255 22.9995 21.2547 22.2047 21.2547 21.3759C21.2547 20.5471 20.9255 19.7522 20.3394 19.1661C19.7534 18.5801 18.9585 18.2509 18.1297 18.2509Z" stroke="#0086C9" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9538_418898">
|
||||
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
11
app/assets/ico/wizard/import.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="37" height="40" viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8817 5.28003C22.6592 5.28003 28.9649 11.5865 28.9649 19.365C28.9649 27.1435 22.6592 33.45 14.8817 33.45C7.10065 33.4535 0.794922 27.1435 0.794922 19.365C0.794922 11.5865 7.10065 5.28003 14.8817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9540_418847)">
|
||||
<path d="M17.3803 22.0008L14.8803 19.5008M14.8803 19.5008L12.3803 22.0008M14.8803 19.5008V25.1258M20.124 23.4946C20.7336 23.1623 21.2152 22.6364 21.4927 22C21.7702 21.3636 21.8279 20.6529 21.6567 19.98C21.4854 19.3072 21.095 18.7105 20.547 18.2842C19.9989 17.858 19.3246 17.6263 18.6303 17.6258H17.8428C17.6536 16.8941 17.301 16.2148 16.8115 15.639C16.322 15.0631 15.7083 14.6058 15.0166 14.3012C14.3249 13.9967 13.5731 13.8529 12.8179 13.8808C12.0626 13.9086 11.3235 14.1073 10.656 14.4619C9.98859 14.8165 9.41023 15.3178 8.96442 15.9281C8.51862 16.5384 8.21697 17.2418 8.08215 17.9855C7.94733 18.7291 7.98285 19.4937 8.18604 20.2216C8.38924 20.9496 8.75481 21.622 9.25529 22.1883" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9540_418847">
|
||||
<rect width="15" height="15" fill="white" transform="translate(7.37988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
4
app/assets/ico/wizard/socket.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path d="M14.3797 23.5025C16.98 23.5025 19.0879 21.3946 19.0879 18.7943L19.0879 16.3519L16.7331 16.3519M14.3797 23.5025C11.7795 23.5025 9.67156 21.3946 9.67156 18.7943L9.67156 16.3519L16.7331 16.3519M14.3797 23.5025C14.3797 24.9593 14.3797 26.5726 16.7331 26.5726L17.7794 26.5726M16.7331 16.3519L16.7331 13.4274M12.0216 16.3519L12.0216 13.4274" stroke="#0086C9" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 768 B |
1
app/assets/ico/zap.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -96,94 +96,6 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||
},
|
||||
};
|
||||
|
||||
var containers = {
|
||||
name: 'docker.containers',
|
||||
url: '/containers',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/containers.html',
|
||||
controller: 'ContainersController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var container = {
|
||||
name: 'docker.containers.container',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/edit/container.html',
|
||||
controller: 'ContainerController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerAttachConsole = {
|
||||
name: 'docker.containers.container.attach',
|
||||
url: '/attach',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/console/attach.html',
|
||||
controller: 'ContainerConsoleController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerExecConsole = {
|
||||
name: 'docker.containers.container.exec',
|
||||
url: '/exec',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/console/exec.html',
|
||||
controller: 'ContainerConsoleController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerCreation = {
|
||||
name: 'docker.containers.new',
|
||||
url: '/new?nodeName&from',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/create/createcontainer.html',
|
||||
controller: 'CreateContainerController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerInspect = {
|
||||
name: 'docker.containers.container.inspect',
|
||||
url: '/inspect',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/inspect/containerinspect.html',
|
||||
controller: 'ContainerInspectController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerLogs = {
|
||||
name: 'docker.containers.container.logs',
|
||||
url: '/logs',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/logs/containerlogs.html',
|
||||
controller: 'ContainerLogsController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var containerStats = {
|
||||
name: 'docker.containers.container.stats',
|
||||
url: '/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containers/stats/containerstats.html',
|
||||
controller: 'ContainerStatsController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const customTemplates = {
|
||||
name: 'docker.templates.custom',
|
||||
url: '/custom',
|
||||
@@ -613,14 +525,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||
$stateRegistryProvider.register(configs);
|
||||
$stateRegistryProvider.register(config);
|
||||
$stateRegistryProvider.register(configCreation);
|
||||
$stateRegistryProvider.register(containers);
|
||||
$stateRegistryProvider.register(container);
|
||||
$stateRegistryProvider.register(containerExecConsole);
|
||||
$stateRegistryProvider.register(containerAttachConsole);
|
||||
$stateRegistryProvider.register(containerCreation);
|
||||
$stateRegistryProvider.register(containerInspect);
|
||||
$stateRegistryProvider.register(containerLogs);
|
||||
$stateRegistryProvider.register(containerStats);
|
||||
|
||||
$stateRegistryProvider.register(customTemplates);
|
||||
$stateRegistryProvider.register(customTemplatesNew);
|
||||
$stateRegistryProvider.register(customTemplatesEdit);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer-alt" title-text="Cluster information"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<rd-widget-header icon="svg-tachometer" title-text="Cluster information"></rd-widget-header>
|
||||
<rd-widget-body classes="!px-5 !py-0">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
@@ -28,7 +30,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="config-removeConfigButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>Remove
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -66,10 +66,8 @@
|
||||
button-spinner="$ctrl.leaveNetworkActionInProgress"
|
||||
ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)"
|
||||
>
|
||||
<span ng-hide="$ctrl.leaveNetworkActionInProgress"
|
||||
><pr-icon icon="'trash-2'" feather="true" mode="'danger'" class-name="'icon-secondary icon-md'"></pr-icon> Leave network</span
|
||||
>
|
||||
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
|
||||
<span ng-if="!$ctrl.leaveNetworkActionInProgress" class="vertical-center !ml-0"> <pr-icon icon="'trash-2'" feather="true"></pr-icon> Leave network</span>
|
||||
<span ng-if="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar show-dropdown">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
@@ -29,7 +31,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="image-removeImageButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>Remove
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -46,6 +48,33 @@
|
||||
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-light h-fit"
|
||||
ui-sref="docker.images.import"
|
||||
authorization="DockerImageLoad"
|
||||
ng-disabled="$ctrl.exportInProgress"
|
||||
data-cy="image-importImageButton"
|
||||
>
|
||||
<pr-icon icon="'upload'" feather="true"></pr-icon>Import
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-light h-fit"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.exportInProgress"
|
||||
ng-click="$ctrl.downloadAction($ctrl.state.selectedItems)"
|
||||
button-spinner="$ctrl.exportInProgress"
|
||||
authorization="DockerImageGet"
|
||||
data-cy="image-exportImageButton"
|
||||
>
|
||||
<pr-icon icon="'download'" feather="true"></pr-icon>
|
||||
<span ng-hide="$ctrl.exportInProgress">Export</span>
|
||||
<span ng-show="$ctrl.exportInProgress">Export in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary h-fit vertical-center !ml-0"
|
||||
@@ -55,31 +84,6 @@
|
||||
>
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon>Build a new image
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary h-fit vertical-center !ml-0"
|
||||
ui-sref="docker.images.import"
|
||||
authorization="DockerImageLoad"
|
||||
ng-disabled="$ctrl.exportInProgress"
|
||||
data-cy="image-importImageButton"
|
||||
>
|
||||
<pr-icon icon="'upload'" feather="true" mode="'secondary'"></pr-icon>Import
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary h-fit vertical-center !ml-0"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.exportInProgress"
|
||||
ng-click="$ctrl.downloadAction($ctrl.state.selectedItems)"
|
||||
button-spinner="$ctrl.exportInProgress"
|
||||
authorization="DockerImageGet"
|
||||
data-cy="image-exportImageButton"
|
||||
>
|
||||
<pr-icon icon="'download'" feather="true" mode="'secondary'"></pr-icon>
|
||||
<span ng-hide="$ctrl.exportInProgress">Export</span>
|
||||
<span ng-show="$ctrl.exportInProgress">Export in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
@@ -28,7 +30,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="network-removeNetworkButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>Remove
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
@@ -28,7 +30,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="secret-removeSecretButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>Remove
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<div class="service-datatable">
|
||||
<div class="inner-datatable">
|
||||
<table class="table table-condensed table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" style="width: 10%">
|
||||
<a ng-click="$ctrl.changeOrderBy('Status.State')">
|
||||
Status
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status.State' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
<span class="space-left">
|
||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
<div class="dropdown-menu" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Filter by state </div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
|
||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" class="w-[10%]">
|
||||
<div class="flex">
|
||||
<table-column-header
|
||||
col-title="'Status'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Status.State'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Status.State')"
|
||||
></table-column-header>
|
||||
<span class="space-left">
|
||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
<div class="dropdown-menu" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Filter by state </div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
ng-click="$ctrl.updateAction($ctrl.selectedItems)"
|
||||
data-cy="service-updateServiceButton"
|
||||
>
|
||||
<pr-icon icon="'refresh-cw'" feather="true" mode="'secondary'"></pr-icon>Update
|
||||
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Update
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -19,7 +19,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.selectedItems)"
|
||||
data-cy="service-removeServiceButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>Remove
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new" ng-if="$ctrl.showAddAction" authorization="DockerServiceCreate">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true" class-name="'icon-nested-blue'" mode="'primary'"></pr-icon>
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
|
||||
</div>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
@@ -28,7 +30,7 @@
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="volume-removeVolumeButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true" mode="'danger'" class="leading-none"></pr-icon>Remove
|
||||
<pr-icon icon="'trash-2'" feather="true" class="leading-none"></pr-icon>Remove
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
title="Search image on Docker Hub"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fab fa-docker text-blue-6"></i> Search
|
||||
<pr-icon icon="'svg-docker'" size="'lg'"></pr-icon> Search
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -47,10 +47,7 @@
|
||||
<div ng-if="!$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<span class="small">
|
||||
<p class="text-muted ml-4">
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'" feather="true"></pr-icon>
|
||||
When using advanced mode, image and repository <b>must be</b> publicly available.
|
||||
</p>
|
||||
<p class="text-muted ml-4"> When using advanced mode, image and repository <b>must be</b> publicly available. </p>
|
||||
</span>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left col-sm-3 col-lg-2 required">Image </label>
|
||||
<div ng-class="$ctrl.inputClass" class="col-sm-8">
|
||||
@@ -61,10 +58,11 @@
|
||||
<!-- ! don't use registry -->
|
||||
<!-- info message -->
|
||||
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
|
||||
<div class="col-sm-12 small">
|
||||
<div ng-messages="$ctrl.form.image_name.$error">
|
||||
<p ng-message="required">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Image name is required.
|
||||
<div class="small">
|
||||
<div class="col-sm-3 col-lg-2"></div>
|
||||
<div class="col-sm-8" ng-messages="$ctrl.form.image_name.$error">
|
||||
<p class="text-muted vertical-center" ng-message="required">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true" class="vertical-center"></pr-icon> Image name is required.
|
||||
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="cifsInformationForm.cifs_address.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="cifsInformationForm.cifs_address.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="cifsInformationForm.cifs_share.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="cifsInformationForm.cifs_share.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="cifsInformationForm.cifs_version.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="cifsInformationForm.cifs_version.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="cifsInformationForm.cifs_username.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="cifsInformationForm.cifs_username.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="cifsInformationForm.password.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="cifsInformationForm.cifs_password.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="nfsInformationForm.nfs_address.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="nfsInformationForm.nfs_address.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="nfsInformationForm.nfs_version.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="nfsInformationForm.nfs_version.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="nfsInformationForm.nfs_mountpoint.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="nfsInformationForm.nfs_mountpoint.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="nfsInformationForm.nfs_options.$invalid">
|
||||
<div class="small">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="nfsInformationForm.nfs_options.$error">
|
||||
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { ContainersDatatableContainer } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableContainer';
|
||||
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.docker.react.components', [])
|
||||
.component(
|
||||
'containersDatatable',
|
||||
r2a(ContainersDatatableContainer, [
|
||||
'endpoint',
|
||||
'isAddActionVisible',
|
||||
'dataset',
|
||||
'onRefresh',
|
||||
'isHostColumnVisible',
|
||||
'tableKey',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'containerQuickActions',
|
||||
r2a(ContainerQuickActions, [
|
||||
@@ -30,4 +19,8 @@ export const componentsModule = angular
|
||||
])
|
||||
)
|
||||
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||
.component('templateListSort', TemplateListSortAngular).name;
|
||||
.component('templateListSort', TemplateListSortAngular)
|
||||
.component(
|
||||
'stackContainersDatatable',
|
||||
r2a(StackContainersDatatable, ['environment', 'stackName'])
|
||||
).name;
|
||||
|
||||
101
app/docker/react/views/containers.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { StateRegistry } from '@uirouter/angularjs';
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { ListView } from '@/react/docker/containers/ListView';
|
||||
|
||||
export const containersModule = angular
|
||||
.module('portainer.docker.containers', [])
|
||||
.component('containersView', r2a(ListView, ['endpoint']))
|
||||
|
||||
.config(config).name;
|
||||
|
||||
/* @ngInject */
|
||||
function config($stateRegistryProvider: StateRegistry) {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers',
|
||||
url: '/containers',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'containersView',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container',
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/edit/container.html',
|
||||
controller: 'ContainerController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container.attach',
|
||||
url: '/attach',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/console/attach.html',
|
||||
controller: 'ContainerConsoleController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container.exec',
|
||||
url: '/exec',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/console/exec.html',
|
||||
controller: 'ContainerConsoleController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.new',
|
||||
url: '/new?nodeName&from',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/create/createcontainer.html',
|
||||
controller: 'CreateContainerController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container.inspect',
|
||||
url: '/inspect',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/inspect/containerinspect.html',
|
||||
controller: 'ContainerInspectController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container.logs',
|
||||
url: '/logs',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/logs/containerlogs.html',
|
||||
controller: 'ContainerLogsController',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'docker.containers.container.stats',
|
||||
url: '/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/stats/containerstats.html',
|
||||
controller: 'ContainerStatsController',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import { components } from 'react-select';
|
||||
import { OnChangeValue } from 'react-select/dist/declarations/src/types';
|
||||
import { components, MultiValue } from 'react-select';
|
||||
import { MultiValueRemoveProps } from 'react-select/dist/declarations/src/components/MultiValue';
|
||||
import {
|
||||
ActionMeta,
|
||||
OnChangeValue,
|
||||
} from 'react-select/dist/declarations/src/types';
|
||||
import { OptionProps } from 'react-select/dist/declarations/src/components/Option';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
@@ -9,6 +13,7 @@ import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
interface Values {
|
||||
enabled: boolean;
|
||||
useSpecific: boolean;
|
||||
selectedGPUs: string[];
|
||||
capabilities: string[];
|
||||
}
|
||||
@@ -81,6 +86,17 @@ function Option(props: OptionProps<GpuOption, true>) {
|
||||
);
|
||||
}
|
||||
|
||||
function MultiValueRemove(props: MultiValueRemoveProps<GpuOption, true>) {
|
||||
const {
|
||||
selectProps: { value },
|
||||
} = props;
|
||||
if (value && (value as MultiValue<GpuOption>).length === 1) {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <components.MultiValueRemove {...props} />;
|
||||
}
|
||||
|
||||
export function Gpu({
|
||||
values,
|
||||
onChange,
|
||||
@@ -97,6 +113,11 @@ export function Gpu({
|
||||
: gpu.name,
|
||||
}));
|
||||
|
||||
options.unshift({
|
||||
value: 'all',
|
||||
label: 'Use All GPUs',
|
||||
});
|
||||
|
||||
return options;
|
||||
}, [gpus, usedGpus, usedAllGpus]);
|
||||
|
||||
@@ -112,11 +133,22 @@ export function Gpu({
|
||||
onChangeValues('enabled', !values.enabled);
|
||||
}
|
||||
|
||||
function onChangeSelectedGpus(newValue: OnChangeValue<GpuOption, true>) {
|
||||
onChangeValues(
|
||||
'selectedGPUs',
|
||||
newValue.map((option) => option.value)
|
||||
);
|
||||
function onChangeSelectedGpus(
|
||||
newValue: OnChangeValue<GpuOption, true>,
|
||||
actionMeta: ActionMeta<GpuOption>
|
||||
) {
|
||||
let { useSpecific } = values;
|
||||
let selectedGPUs = newValue.map((option) => option.value);
|
||||
|
||||
if (actionMeta.action === 'select-option') {
|
||||
useSpecific = actionMeta.option?.value !== 'all';
|
||||
selectedGPUs = selectedGPUs.filter((value) =>
|
||||
useSpecific ? value !== 'all' : value === 'all'
|
||||
);
|
||||
}
|
||||
|
||||
const newValues = { ...values, selectedGPUs, useSpecific };
|
||||
onChange(newValues);
|
||||
}
|
||||
|
||||
function onChangeSelectedCaps(newValue: OnChangeValue<GpuOption, true>) {
|
||||
@@ -128,8 +160,9 @@ export function Gpu({
|
||||
|
||||
const gpuCmd = useMemo(() => {
|
||||
const devices = values.selectedGPUs.join(',');
|
||||
const deviceStr = devices === 'all' ? 'all,' : `device=${devices},`;
|
||||
const caps = values.capabilities.join(',');
|
||||
return `--gpus 'device=${devices},"capabilities=${caps}"`;
|
||||
return `--gpus '${deviceStr}"capabilities=${caps}"'`;
|
||||
}, [values.selectedGPUs, values.capabilities]);
|
||||
|
||||
const gpuValue = useMemo(
|
||||
@@ -164,9 +197,12 @@ export function Gpu({
|
||||
isMulti
|
||||
closeMenuOnSelect
|
||||
value={gpuValue}
|
||||
isClearable={false}
|
||||
backspaceRemovesValue={false}
|
||||
isDisabled={!values.enabled}
|
||||
onChange={onChangeSelectedGpus}
|
||||
options={options}
|
||||
components={{ MultiValueRemove }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,11 +212,7 @@ export function Gpu({
|
||||
<div className="form-group">
|
||||
<div className="col-sm-3 col-lg-2 control-label text-left">
|
||||
Capabilities
|
||||
<Tooltip
|
||||
message={
|
||||
"This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings."
|
||||
}
|
||||
/>
|
||||
<Tooltip message="‘compute’ and ‘utility’ capabilities are preselected by Portainer because they are used by default when you don’t explicitly specify capabilities with docker CLI ‘--gpus’ option." />
|
||||
</div>
|
||||
<div className="col-sm-9 col-lg-10 text-left">
|
||||
<Select<GpuOption, true>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import angular from 'angular';
|
||||
import { Gpu } from 'Docker/react/views/gpu';
|
||||
|
||||
import { ItemView } from '@/react/docker/networks/ItemView';
|
||||
import { ItemView as NetworksItemView } from '@/react/docker/networks/ItemView';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
import { containersModule } from './containers';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.docker.react.views', [])
|
||||
.module('portainer.docker.react.views', [containersModule])
|
||||
.component(
|
||||
'gpu',
|
||||
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
||||
)
|
||||
.component('networkDetailsView', r2a(ItemView, [])).name;
|
||||
.component('networkDetailsView', r2a(NetworksItemView, [])).name;
|
||||
|
||||
@@ -36,20 +36,6 @@ export async function getInfo(environmentId: EnvironmentId) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
action: string,
|
||||
subAction = ''
|
||||
) {
|
||||
let url = `/endpoints/${environmentId}/docker/${action}`;
|
||||
|
||||
if (subAction) {
|
||||
url += `/${subAction}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useInfo<TSelect = InfoResponse>(
|
||||
environmentId: EnvironmentId,
|
||||
select?: (info: InfoResponse) => TSelect
|
||||
@@ -74,3 +60,17 @@ export function useVersion<TSelect = VersionResponse>(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
action: string,
|
||||
subAction = ''
|
||||
) {
|
||||
let url = `/endpoints/${environmentId}/docker/${action}`;
|
||||
|
||||
if (subAction) {
|
||||
url += `/${subAction}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ class CreateConfigController {
|
||||
const resourceControl = data.Portainer.ResourceControl;
|
||||
const userId = userDetails.ID;
|
||||
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
this.Notifications.success('Config successfully created');
|
||||
this.Notifications.success('Success', 'Configuration successfully created');
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('docker.configs', {}, { reload: true });
|
||||
} catch (err) {
|
||||
|
||||
@@ -16,7 +16,7 @@ angular.module('portainer.docker').controller('ConfigController', [
|
||||
$scope.removeConfig = function removeConfig(configId) {
|
||||
ConfigService.remove(configId)
|
||||
.then(function success() {
|
||||
Notifications.success('Config successfully removed');
|
||||
Notifications.success('Success', 'Configuration successfully removed');
|
||||
$state.go('docker.configs', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="terminal" feather-icon="true" title-text="Attach"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="small" ng-if="!container.Config.OpenStdin">
|
||||
<div class="small text-warning" ng-if="!container.Config.OpenStdin">
|
||||
<p>
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
The interactive-flag is not set. You might not be able to use the console properly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small" ng-if="!container.Config.Tty">
|
||||
<div class="small text-warning" ng-if="!container.Config.Tty">
|
||||
<p>
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
The TTY-flag is not set. You might not be able to use the console properly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small text-danger" ng-hide="container.State.Running">
|
||||
<div class="small text-warning" ng-hide="container.State.Running">
|
||||
<p>
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
The container is not running.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<page-header title="'Container list'" breadcrumbs="['Containers']" reload="true"> </page-header>
|
||||
|
||||
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" ng-if="containers">
|
||||
<containers-datatable
|
||||
endpoint="endpoint"
|
||||
dataset="containers"
|
||||
is-host-column-visible="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||
is-add-action-visible="true"
|
||||
on-refresh="(getContainers)"
|
||||
></containers-datatable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,60 +0,0 @@
|
||||
angular.module('portainer.docker').controller('ContainersController', ContainersController);
|
||||
import _ from 'lodash';
|
||||
|
||||
/* @ngInject */
|
||||
function ContainersController($scope, ContainerService, Notifications, endpoint) {
|
||||
$scope.offlineMode = endpoint.Status !== 1;
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.getContainers = getContainers;
|
||||
|
||||
function getContainers() {
|
||||
$scope.containers = null;
|
||||
$scope.containers_t = null;
|
||||
ContainerService.containers(1)
|
||||
.then(function success(data) {
|
||||
$scope.containers_t = data;
|
||||
if ($scope.containers_t.length === 0) {
|
||||
$scope.containers = $scope.containers_t;
|
||||
return;
|
||||
}
|
||||
for (let item of $scope.containers_t) {
|
||||
ContainerService.container(item.Id).then(function success(data) {
|
||||
var Id = data.Id;
|
||||
for (var i = 0; i < $scope.containers_t.length; i++) {
|
||||
if (Id == $scope.containers_t[i].Id) {
|
||||
const gpuOptions = _.find(data.HostConfig.DeviceRequests, function (o) {
|
||||
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
||||
});
|
||||
if (!gpuOptions) {
|
||||
$scope.containers_t[i]['Gpus'] = 'none';
|
||||
} else {
|
||||
let gpuStr = 'all';
|
||||
if (gpuOptions.Count !== -1) {
|
||||
gpuStr = `id:${_.join(gpuOptions.DeviceIDs, ',')}`;
|
||||
}
|
||||
$scope.containers_t[i]['Gpus'] = `${gpuStr}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let item of $scope.containers_t) {
|
||||
if (!Object.keys(item).includes('Gpus')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$scope.containers = $scope.containers_t;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
||||
$scope.containers = [];
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
getContainers();
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
||||
@@ -72,7 +72,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
GPU: {
|
||||
enabled: false,
|
||||
useSpecific: false,
|
||||
selectedGPUs: [],
|
||||
selectedGPUs: ['all'],
|
||||
capabilities: ['compute', 'utility'],
|
||||
},
|
||||
Console: 'none',
|
||||
@@ -469,28 +469,24 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
function prepareGPUOptions(config) {
|
||||
const driver = 'nvidia';
|
||||
const gpuOptions = $scope.formValues.GPU;
|
||||
|
||||
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
|
||||
return o.Driver === driver || o.Capabilities[0][0] === 'gpu';
|
||||
});
|
||||
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: driver });
|
||||
if (existingDeviceRequest) {
|
||||
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
|
||||
}
|
||||
|
||||
if (!gpuOptions.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceRequest = existingDeviceRequest || {
|
||||
const deviceRequest = {
|
||||
Driver: driver,
|
||||
Count: -1,
|
||||
DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
|
||||
Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
|
||||
// Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
|
||||
};
|
||||
|
||||
deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
|
||||
deviceRequest.Count = 0;
|
||||
if (gpuOptions.useSpecific) {
|
||||
deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
|
||||
deviceRequest.Count = 0;
|
||||
}
|
||||
deviceRequest.Capabilities = [gpuOptions.capabilities];
|
||||
|
||||
config.HostConfig.DeviceRequests.push(deviceRequest);
|
||||
@@ -661,6 +657,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs || [];
|
||||
if ($scope.formValues.GPU.useSpecific) {
|
||||
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs;
|
||||
} else {
|
||||
$scope.formValues.GPU.selectedGPUs = ['all'];
|
||||
}
|
||||
// we only support a single set of capabilities for now
|
||||
// UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||
@@ -883,7 +881,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
} else {
|
||||
await ContainerService.updateLimits($transition$.params().from, config);
|
||||
$scope.config = config;
|
||||
Notifications.success('Limits updated');
|
||||
Notifications.success('Success', 'Limits updated');
|
||||
}
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Update Limits fail');
|
||||
@@ -1090,7 +1088,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
Notifications.success('Container successfully created');
|
||||
Notifications.success('Success', 'Container successfully created');
|
||||
$state.go('docker.containers', {}, { reload: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title"> Image configuration </div>
|
||||
<div ng-if="!formValues.RegistryModel.Registry && fromContainer">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
<span class="small text-danger" style="margin-left: 5px">
|
||||
The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that
|
||||
registry first.
|
||||
@@ -39,10 +39,10 @@
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
on-image-change="onImageNameChange()"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="formValues.alwaysPull"
|
||||
on-image-change="onImageNameChange()"
|
||||
set-validity="setPullImageValidity"
|
||||
>
|
||||
<!-- always-pull -->
|
||||
@@ -63,6 +63,7 @@
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
</div>
|
||||
|
||||
<!-- create-webhook -->
|
||||
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
@@ -127,8 +128,8 @@
|
||||
<!-- protocol-actions -->
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
|
||||
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
|
||||
<label class="btn btn-light" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
|
||||
<label class="btn btn-light" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
|
||||
</div>
|
||||
<button class="btn btn-light" type="button" ng-click="removePortBinding($index)">
|
||||
<pr-icon icon="'trash-2'" feather="true" class-name="'icon-secondary icon-md'"></pr-icon>
|
||||
@@ -170,7 +171,7 @@
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||
ng-click="create()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
@@ -213,8 +214,8 @@
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'default'" style="margin-left: 0px"> Default</button>
|
||||
<button class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'override'">Override</button>
|
||||
<label class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'default'" style="margin-left: 0px"> Default</label>
|
||||
<label class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'override'">Override</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -348,8 +349,8 @@
|
||||
<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 class="btn btn-light" type="button" ng-click="removeLogDriverOpt($index)">
|
||||
<pr-icon icon="'trash-2'" feather="true" class-name="'icon-secondary icon-md'"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,7 +290,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
function removeContainer(cleanAssociatedVolumes) {
|
||||
ContainerService.remove($scope.container, cleanAssociatedVolumes)
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully removed');
|
||||
Notifications.success('Success', 'Container successfully removed');
|
||||
$state.go('docker.containers', {}, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
@@ -380,7 +380,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
}
|
||||
|
||||
function notifyAndChangeView() {
|
||||
Notifications.success('Container successfully re-created');
|
||||
Notifications.success('Success', 'Container successfully re-created');
|
||||
$state.go('docker.containers', {}, { reload: true });
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
Name: restartPolicy,
|
||||
MaximumRetryCount: maximumRetryCount,
|
||||
};
|
||||
Notifications.success('Restart policy updated');
|
||||
Notifications.success('Success', 'Restart policy updated');
|
||||
}
|
||||
|
||||
function notifyOnError(err) {
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
>
|
||||
<span class="small">
|
||||
<p class="text-muted" ng-if="applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'" feather="true"></pr-icon>
|
||||
Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look at
|
||||
<a href="http://portainer.readthedocs.io/en/stable/agent.html" target="_blank">our agent setup</a> for more details.
|
||||
</p>
|
||||
<p class="text-muted" ng-if="applicationState.endpoint.mode.role === 'WORKER'">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'" feather="true"></pr-icon>
|
||||
Portainer is connected to a worker node. Swarm management features will not be available.
|
||||
</p>
|
||||
</span>
|
||||
@@ -32,8 +32,8 @@
|
||||
<div class="row" ng-if="(!applicationState.endpoint.mode.agentProxy || applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE') && info && endpoint">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer-alt" title-text="Environment info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<rd-widget-header icon="svg-tachometer" title-text="Environment info"></rd-widget-header>
|
||||
<rd-widget-body classes="!px-5 !py-0">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -41,7 +41,8 @@
|
||||
<td>
|
||||
{{ endpoint.Name }}
|
||||
<span class="small text-muted space-left">
|
||||
<i class="fa fa-microchip"></i> {{ endpoint.Snapshots[0].TotalCPU }} <i class="fa fa-memory space-left"></i> {{ endpoint.Snapshots[0].TotalMemory | humansize }}
|
||||
<pr-icon icon="'cpu'" feather="true"></pr-icon> {{ endpoint.Snapshots[0].TotalCPU }} <pr-icon icon="'svg-memory'"></pr-icon>
|
||||
{{ endpoint.Snapshots[0].TotalMemory | humansize }}
|
||||
</span>
|
||||
<span class="small text-muted">
|
||||
- {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }}
|
||||
@@ -77,33 +78,33 @@
|
||||
|
||||
<div class="dashboard-grid mx-4">
|
||||
<a ui-sref="docker.stacks" ng-if="showStacks">
|
||||
<dashboard-item icon="'layers'" feather-icon="true" type="'Stack'" value="stackCount"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'layers'" feather-icon="true" type="'Stack'" value="stackCount"></dashboard-item>
|
||||
</a>
|
||||
|
||||
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<a ui-sref="docker.services">
|
||||
<dashboard-item icon="'fa-list-alt'" type="'Service'" value="serviceCount"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'shuffle'" type="'Service'" value="serviceCount"></dashboard-item>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a ng-if="containers" ui-sref="docker.containers">
|
||||
<dashboard-item icon="'fa-cubes'" type="'Container'" value="containers.length" children="containerStatusComponent"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'box'" type="'Container'" value="containers.length" children="containerStatusComponent"></dashboard-item>
|
||||
</a>
|
||||
|
||||
<a ng-if="images" ui-sref="docker.images">
|
||||
<dashboard-item icon="'fa-clone'" type="'Image'" value="images.length" children="imagesTotalSizeComponent"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'list'" type="'Image'" value="images.length" children="imagesTotalSizeComponent"></dashboard-item>
|
||||
</a>
|
||||
|
||||
<a ui-sref="docker.volumes">
|
||||
<dashboard-item icon="'fa-hdd'" type="'Volume'" value="volumeCount"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'database'" type="'Volume'" value="volumeCount"></dashboard-item>
|
||||
</a>
|
||||
|
||||
<a ui-sref="docker.networks">
|
||||
<dashboard-item icon="'fa-sitemap'" type="'Network'" value="networkCount"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'share2'" type="'Network'" value="networkCount"></dashboard-item>
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<dashboard-item icon="'fa-digital-tachograph'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
|
||||
<dashboard-item feather-icon="true" icon="'cpu'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@ export default class DockerFeaturesConfigurationController {
|
||||
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
|
||||
|
||||
this.endpoint.SecuritySettings = securitySettings;
|
||||
this.Notifications.success('Saved settings successfully');
|
||||
this.Notifications.success('Success', 'Saved settings successfully');
|
||||
} catch (e) {
|
||||
this.Notifications.error('Failure', e, 'Failed saving settings');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
angular.module('portainer.docker').controller('BuildImageController', BuildImageController);
|
||||
|
||||
/* @ngInject */
|
||||
function BuildImageController($scope, $async, $window, ModalService, BuildService, Notifications, HttpRequestHelper) {
|
||||
$scope.state = {
|
||||
BuildType: 'editor',
|
||||
@@ -9,7 +10,7 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
ImageNames: [{ Name: '' }],
|
||||
ImageNames: [{ Name: '', Valid: false, Unique: true }],
|
||||
UploadFile: null,
|
||||
DockerFileContent: '',
|
||||
URL: '',
|
||||
@@ -27,19 +28,38 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||
$scope.state.isEditorDirty = false;
|
||||
});
|
||||
|
||||
$scope.checkName = function (name) {
|
||||
const parts = name.split('/');
|
||||
$scope.checkName = function (index) {
|
||||
var item = $scope.formValues.ImageNames[index];
|
||||
item.Valid = true;
|
||||
item.Unique = true;
|
||||
if (item.Name !== '') {
|
||||
// Check unique
|
||||
$scope.formValues.ImageNames.forEach((element, idx) => {
|
||||
if (idx != index && element.Name == item.Name) {
|
||||
item.Valid = false;
|
||||
item.Unique = false;
|
||||
}
|
||||
});
|
||||
if (!item.Valid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Validation
|
||||
const parts = item.Name.split('/');
|
||||
const repository = parts[parts.length - 1];
|
||||
const repositoryRegExp = RegExp('^[a-z0-9-_]{2,255}(:[A-Za-z0-9-_.]{1,128})?$');
|
||||
return repositoryRegExp.test(repository);
|
||||
item.Valid = repositoryRegExp.test(repository);
|
||||
};
|
||||
|
||||
$scope.addImageName = function () {
|
||||
$scope.formValues.ImageNames.push({ Name: '' });
|
||||
$scope.formValues.ImageNames.push({ Name: '', Valid: false, Unique: true });
|
||||
};
|
||||
|
||||
$scope.removeImageName = function (index) {
|
||||
$scope.formValues.ImageNames.splice(index, 1);
|
||||
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
|
||||
$scope.checkName(i);
|
||||
}
|
||||
};
|
||||
|
||||
function buildImageBasedOnBuildType(method, names) {
|
||||
@@ -103,8 +123,7 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
|
||||
var item = $scope.formValues.ImageNames[i];
|
||||
if (!$scope.checkName(item.Name)) {
|
||||
if (!$scope.formValues.ImageNames[i].Valid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,26 +40,33 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 form-inline" class="mt-2.5">
|
||||
<div ng-repeat="item in formValues.ImageNames track by $index" class="mt-0.5">
|
||||
<div ng-repeat="item in formValues.ImageNames track by $index" class="mt-1">
|
||||
<!-- name-input -->
|
||||
<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="item.Name" placeholder="e.g. my-image:my-tag" auto-focus />
|
||||
<span class="input-group-addon" ng-if="!checkName(item.Name)">
|
||||
<input type="text" class="form-control" ng-model="item.Name" ng-change="checkName($index)" placeholder="e.g. my-image:my-tag" auto-focus />
|
||||
<span class="input-group-addon" ng-if="!item.Valid">
|
||||
<pr-icon icon="'x'" mode="'danger'" feather="true"></pr-icon>
|
||||
</span>
|
||||
<span class="input-group-addon" ng-if="checkName(item.Name)">
|
||||
<span class="input-group-addon" ng-if="item.Valid">
|
||||
<pr-icon icon="'check'" mode="'success'" feather="true"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- actions -->
|
||||
<div class="input-group col-sm-2 input-group-sm">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeImageName($index)">
|
||||
<pr-icon icon="'trash-2'" feather="true" class-name="'icon-secondary icon-md'"></pr-icon>
|
||||
<button class="btn btn-dangerlight btn-only-icon" type="button" ng-click="removeImageName($index)">
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
<div class="small text-warning" ng-if="!item.Valid">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
<span ng-if="!item.Unique">The image name must be unique</span>
|
||||
<span ng-if="item.Unique"
|
||||
>The image name must consist of between 2 and 255 lowercase alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +211,8 @@
|
||||
ng-disabled="state.actionInProgress
|
||||
|| (state.BuildType === 'upload' && (!formValues.UploadFile || !formValues.Path))
|
||||
|| (state.BuildType === 'url' && (!formValues.URL || !formValues.Path))
|
||||
|| (formValues.ImageNames.length === 0 || !validImageNames())"
|
||||
|| (formValues.ImageNames.length === 0 || !validImageNames())
|
||||
|| (formValues.DockerFileContent === '')"
|
||||
ng-click="buildImage()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
|
||||
@@ -76,7 +76,7 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
|
||||
ImageService.tagImage($transition$.params().id, image.fromImage)
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully tagged');
|
||||
Notifications.success('Success', 'Image successfully tagged');
|
||||
$state.go('docker.images.image', { id: $transition$.params().id }, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
@@ -155,7 +155,7 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
.then(function success(data) {
|
||||
var downloadData = new Blob([data.file], { type: 'application/x-tar' });
|
||||
FileSaver.saveAs(downloadData, 'images.tar');
|
||||
Notifications.success('Image successfully downloaded');
|
||||
Notifications.success('Success', 'Image successfully downloaded');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to download image');
|
||||
|
||||
@@ -90,7 +90,7 @@ angular.module('portainer.docker').controller('ImagesController', [
|
||||
.then(function success(data) {
|
||||
var downloadData = new Blob([data.file], { type: 'application/x-tar' });
|
||||
FileSaver.saveAs(downloadData, 'images.tar');
|
||||
Notifications.success('Image(s) successfully downloaded');
|
||||
Notifications.success('Success', 'Image(s) successfully downloaded');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to download image(s)');
|
||||
|
||||
@@ -58,9 +58,9 @@ angular.module('portainer.docker').controller('ImportImageController', [
|
||||
await tagImage(imageIds[1]);
|
||||
$state.go('docker.images.image', { id: imageIds[1] }, { reload: true });
|
||||
}
|
||||
Notifications.success('Images successfully uploaded');
|
||||
Notifications.success('Success', 'Images successfully uploaded');
|
||||
} else {
|
||||
Notifications.success('The uploaded tar file contained multiple images. The provided tag therefore has been ignored.');
|
||||
Notifications.success('Success', 'The uploaded tar file contained multiple images. The provided tag therefore has been ignored.');
|
||||
}
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to upload image');
|
||||
|
||||
@@ -239,7 +239,7 @@ angular.module('portainer.docker').controller('CreateNetworkController', [
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Network successfully created');
|
||||
Notifications.success('Success', 'Network successfully created');
|
||||
if (context.reload) {
|
||||
$state.go(
|
||||
'docker.networks',
|
||||
|
||||
@@ -19,7 +19,7 @@ class DockerRegistryAccessController {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.EndpointService.updateRegistryAccess(this.state.endpointId, this.state.registryId, this.registryEndpointAccesses);
|
||||
this.Notifications.success('Access successfully updated');
|
||||
this.Notifications.success('Success', 'Access successfully updated');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.state.actionInProgress = false;
|
||||
|
||||
@@ -89,7 +89,7 @@ angular.module('portainer.docker').controller('CreateSecretController', [
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Secret successfully created');
|
||||
Notifications.success('Success', 'Secret successfully created');
|
||||
$state.go('docker.secrets', {}, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<td>
|
||||
{{ secret.Id }}
|
||||
<button authorization="DockerSecretDelete" class="btn btn-xs btn-dangerlight" ng-click="removeSecret(secret.Id)"
|
||||
><pr-icon icon="'trash-2'" feather="true" mode="'danger'"></pr-icon>Delete this secret</button
|
||||
><pr-icon icon="'trash-2'" feather="true"></pr-icon>Delete this secret</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -16,7 +16,7 @@ angular.module('portainer.docker').controller('SecretController', [
|
||||
$scope.removeSecret = function removeSecret(secretId) {
|
||||
SecretService.remove(secretId)
|
||||
.then(function success() {
|
||||
Notifications.success('Secret successfully removed');
|
||||
Notifications.success('Success', 'Secret successfully removed');
|
||||
$state.go('docker.secrets', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -537,7 +537,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
return $q.all([rcPromise, webhookPromise]);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Service successfully created');
|
||||
Notifications.success('Success', 'Service successfully created');
|
||||
$state.go('docker.services', {}, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||