Compare commits

...

59 Commits

Author SHA1 Message Date
deviantony
540afcd179 Merge branch 'develop' into feat-fdo-1.1 2022-08-16 14:33:02 +00:00
matias-portainer
4c23513a41 fix(home): remove edge devices from homepage list EE-3919 (#7471) 2022-08-16 09:57:55 -03:00
Matt Hook
81d1f35bdc fix snapshot url parsing issue for ip addresses (#7478) 2022-08-16 10:36:12 +12:00
Ali
36c93c7f57 fix(ui): kubernetes-consistent-styling EE-3820 (#7425) 2022-08-13 00:22:45 +06:00
Rex Wang
b67f404d8d EE-3905 changes for item 1,2,3,4,9,10,12,13,14 (#7467) 2022-08-12 12:47:44 +08:00
Chaim Lev-Ari
95fb5a4baa fix(ui): fix ui bugs [EE-3847] (#7453) 2022-08-12 15:47:56 +12:00
matias-portainer
dd372637cb feat(ui): renovate the edge devices waiting room (#7456) 2022-08-12 15:01:31 +12:00
Chaim Lev-Ari
c1a4856e9d feat(ui/datatables): add styles for nested tables [EE-3687] (#7440)
* feat(ui/datatables): add styles for nested tables
2022-08-12 14:56:48 +12:00
Chaim Lev-Ari
92b7e64689 feat(ui/sidebar): support custom logos [EE-3753] (#7436)
* feat(ui/sidebar): show right logos
2022-08-12 13:27:30 +12:00
matias-portainer
a750259a2c fix(edge): generate new EdgeID only if not present (#7454) 2022-08-11 22:23:13 -03:00
matias-portainer
87accfce5d fix(edge): parse agent platform on every polling request to avoid endpoint misconfiguration (#7452) 2022-08-11 22:21:56 -03:00
Chaim Lev-Ari
29f0daa7ea fix(edge/stacks): show correct status for env [EE-3374] (#7466) 2022-08-11 22:20:36 -03:00
Richard Wei
a247db7e93 feat(ui): added teaser styling for CE EE-3780 (#7323)
* added teaser styling for CE
2022-08-12 12:03:30 +12:00
itsconquest
1fbaf5fcbf fix(style): correct common pages [EE-3886] (#7449)
* fix(css): correct common pages [EE-3886]
2022-08-12 11:58:31 +12:00
Chaim Lev-Ari
c981e6ff7b fix(home): clear all filters [EE-3912] (#7465) 2022-08-12 02:00:33 +03:00
Richard Wei
ee1ee633d7 feat(ui): portainer wizard ui change for ce EE-3576 (#7405)
* ui change for wizard
2022-08-12 08:43:01 +12:00
Ali
a7ab0a5662 feat(ui): box-selector-style-updates EE-3698 (#7382) 2022-08-11 14:13:11 +06:00
Chaim Lev-Ari
bed4257194 refactor(containers): migrate view to react [EE-2212] (#6577)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-08-11 07:33:29 +03:00
Chaim Lev-Ari
5ee570e075 feat(home): filter by connection type and agent version [EE-3373] (#7085) 2022-08-11 07:32:12 +03:00
Rex Wang
9666c21b8a EE-3860 fix typo (#7447) 2022-08-11 07:40:26 +08:00
Oscar Zhou
5cf789a8e4 fix(yarn): update yarn lock file to fix nightly code scan failure (#7460) 2022-08-11 10:28:58 +12:00
Matt Hook
6a4a353b92 feat(environment): update wording when editing agent environment [EE-3081] (#7445)
* change wording when editing agent environment
2022-08-11 09:27:35 +12:00
Prabhat Khera
02355acfa8 fix(ui): namespace name sort EE-3863 (#7442) 2022-08-11 09:25:29 +12:00
congs
04eb718f88 fix(gpu) EE-3191 fix gpu bugs (#7451) 2022-08-11 09:05:27 +12:00
congs
36888b5ad4 feat(ui): EE-3567 css portainer settings auth (#7423) 2022-08-10 17:49:43 +12:00
Ali
7bd971f838 fix(toast): update styles and custom button (#7450)
EE-3829
2022-08-10 17:07:35 +12:00
Chaim Lev-Ari
c3ce4d8b53 feat(sidebar): add dark theme colors [EE-3666] (#7414) 2022-08-10 07:12:20 +03:00
Ali
fb3a31a4fd feat(login): allow-show-hide-password-in-login-screen EE-3885 (#7433)
* feat(login): allow show/hide password EE-3885
2022-08-10 16:07:24 +12:00
Rex Wang
b6852b5e30 fix(UI) registry page improvement EE-2705 (#7424)
* EE-2705 bug fix

* EE-2705 hide auth switch for gitlab
2022-08-10 09:07:20 +08:00
Richard Wei
34e2178752 feat(ui): portainer registry boxselector icon ce EE-3848 (#7419)
* add icon to registry boxselector
2022-08-10 12:21:17 +12:00
fhanportainer
83a17de1c0 feat(roles): fixed search box in the Roles page. (#7448)
* feat(roles): fixed search box in the Roles page.

* feat(roles): fixed icon position
2022-08-09 19:22:10 +12:00
Oscar Zhou
e5b27d7a57 fix(ldap/tls): allow to upload tls ca certificate [EE-3654] (#7340) 2022-08-09 17:19:32 +12:00
Richard Wei
fb14a85483 fix search box for group (#7446) 2022-08-09 12:43:37 +12:00
Zhang Hao
8d4cb5e16b fix(container): some style bug on container create page [EE-3744] (#7415)
* fix(container): some style bug on container create page [EE-3744]
2022-08-09 07:50:18 +08:00
Richard Wei
ad8b8399c4 fix(ui): remove right label for switch toggle EE-3786 (#7349)
* remove right label for switch toggle
2022-08-08 16:43:31 +12:00
Dakota Walsh
8ff2fa66b6 fix(kube): update kubectl agent install instructions (#7421) 2022-08-08 14:06:10 +12:00
Zhang Hao
539948b5a6 fix(container): fixed add value and remove value for env [EE-3839] (#7429)
* fix(style): UI task issues [EE-3839]
2022-08-05 15:45:21 +08:00
Dmitry Salakhov
bfe1cace77 fix: correctly flagged pull latest image feature as limited (#7428) 2022-08-05 16:05:49 +12:00
Dakota Walsh
7b806cf586 fix(cache): trigger page reload on logout (#7407)
When portainer is restarted the user's session is invalidated and as
soon as they start clicking around they will be logged out. This PR does
two additional things when this happens.

1) We trigger a browser page reload which will force the client the grab
   the latest version of our js, css, etc. Previously if a user updated
   portainer, but never clicked the browser's refresh button they would
   never see new css changes.

2) We also set "cache-control no-cache" on the index.html header. Since
   portainer is an SPA and the the index.html is very small it makes
   sense to avoid letting the browser cache it so that the user is
   always given the latest version when the above reload is triggered.
2022-08-05 15:38:45 +12:00
Rex Wang
69a824c25b Fix(UI) Update UI of docker dashboard EE-3845 (#7422)
* EE-3846 fix alignment of left-hand side of fields
2022-08-05 10:17:31 +08:00
itsconquest
8d733ccc8c style(init): style init views [EE-3556] (#7384)
* style(init): style init views [EE-3556]

* update icons, alignment & allow clicking feature indicator link

* update cursor style on hover
2022-08-05 11:48:08 +12:00
Rex Wang
2574f223b4 fix(UI) check image name when build image EE-3010 (#7409)
* EE-3010 check image name when build image
2022-08-05 07:04:26 +08:00
fhanportainer
f2d93654f5 feat(roles): updated roles view css UI (#7389)
* feat(roles): updated roles view css UI

* feat(roles): updated icons
2022-08-05 10:26:33 +12:00
fhanportainer
5e74b90780 feat(teams): updated teams edit css UI (#7403)
* feat(teams): updated teams edit css UI

* feat(team): removed inline style.
2022-08-05 10:25:29 +12:00
fhanportainer
78ce176268 feat(teams): teams page css UI update. (#7402)
* feat(teams): teams page css UI update.

* feat(teams): added `required` attr to team name field

* feat(teams): fixed remove and search bar position

* feat(teams): fixed CreateTeamForm unit test
2022-08-05 10:24:19 +12:00
Matt Hook
dfb398d091 import the search feature (#7426) 2022-08-05 10:21:26 +12:00
Matt Hook
4e9b3a8940 fix(endpoint handler): fix endpoint address(url) parsing EE-3081] (#7408)
fix address validation when creating agent endpoint
2022-08-05 09:30:54 +12:00
deviantony
64b9207497 refactor(openamt): add review comment 2022-05-11 21:28:17 +00:00
deviantony
b3dbfd1a5e feat(openamt): add comment 2022-05-11 20:58:20 +00:00
deviantony
0f08005982 feat(openamt): fix an openamt issue with v2.1.0 2022-05-10 21:11:37 +00:00
deviantony
587ea7e8ea Merge branch 'develop' into feat-fdo-1.1 2022-05-10 20:51:42 +00:00
deviantony
fe82b23211 use v2.1.0 of oact-rpc-go 2022-05-10 03:18:37 +00:00
deviantony
6099a916b9 feat(openamt): use official intel rpc-go image 2022-05-10 02:41:11 +00:00
deviantony
3d9aae9760 feat(settings): allow empty edge URL 2022-05-10 01:49:20 +00:00
deviantony
1316168b6b Merge branch 'develop' into feat-fdo-1.1 2022-05-09 23:41:28 +00:00
deviantony
88042f9b39 feat(openamt): update saveCIRAConfig logic 2022-05-08 19:14:26 +00:00
deviantony
4515315f41 feat(openamt): update addressFormat logic 2022-05-08 18:32:40 +00:00
deviantony
00bd7c3d02 feat(fdo): update configure logic 2022-05-01 00:01:22 +00:00
deviantony
0b8adb690e feat(fdo): fdo 1.1 changes 2022-04-29 19:41:51 +00:00
472 changed files with 7981 additions and 5055 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache

76
api/agent/version.go Normal file
View 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
}

View File

@@ -27,6 +27,9 @@
],
"endpoints": [
{
"Agent": {
"Version": ""
},
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {

View File

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

View File

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

View 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})
}

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

View 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)
})
}

View File

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

View File

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

View 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())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View 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

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

View 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

View 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

View 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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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 dont explicitly specify capabilities with docker CLI --gpus option." />
</div>
<div className="col-sm-9 col-lg-10 text-left">
<Select<GpuOption, true>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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