Compare commits

...

38 Commits

Author SHA1 Message Date
itsconquest 103fbbfe6e fix(boxselector): fix darkmode BE teaser style [EE-4145] (#7597)
Test / test-client (push) Has been cancelled
* fix(boxselector): fix darkmode BE teaser style [EE-4145]

* make opacity same when selected

* add missing link to teaser

* style unchecked boxes + light mode

* revert colors for ligh theme
2022-09-02 12:42:43 +12:00
Oscar Zhou c8044cc125 fix(stack/compose): remove the orphan containers if stack deployment is failed (#7600) 2022-09-02 08:11:14 +12:00
LP B 154f2ed189 fix(container/edit): fallback value when retrieving GPU config without snapshot available [EE-4110] (#7571) 2022-08-30 14:52:32 +02:00
itsconquest 1a2591f0cf fix(sidebar): rework the update notification [EE-4119] (#7574) 2022-08-30 10:00:15 +12:00
wheresolivia dacd188fa0 add data-cy attributes for docker image tag selectors (#7583) 2022-08-30 08:49:32 +12:00
Chaim Lev-Ari 4be094cb29 feat(ui): hide user menu on docker extension [EE-4115] (#7564) 2022-08-29 05:07:10 +03:00
congs 2c9328d765 fix(stack): EE-3908 broken modal when updating/redeploying stacks: turn off toggle (#7572) 2022-08-26 17:54:03 +12:00
Matt Hook ede9bc6111 fix(swarm): fixed issue parsing url with no scheme [EE-4017] (#7562) 2022-08-26 11:56:08 +12:00
itsconquest 2abb6650ae fix(stacks): orphaned stacks readonly [EE-4085] (#7551)
* fix(stacks): orphaned stacks readonly [EE-4085]

* correctly handle stack type in controller

* Update stackController.js
2022-08-25 10:57:43 +12:00
congs cc73724351 fix(container): EE-3995 gpus console error under stack list page (#7529) 2022-08-25 10:27:08 +12:00
Matt Hook 6fd5ebbf55 fix(web-editor): add search hint text [EE-3967] (#7495) 2022-08-25 10:11:10 +12:00
Chaim Lev-Ari aad0b139fc fix(stack): hide containers for swarm stack [EE-3969] (#7567) 2022-08-24 17:17:58 +03:00
Zhang Hao a8157caa67 fix(image): Add hide default registry teaser for CE version [EE-4038] (#7554) 2022-08-24 19:32:51 +08:00
Chaim Lev-Ari f69a6129e3 fix(ui/buttons): set hyperlink style [EE-4007] (#7523) 2022-08-24 07:40:43 +03:00
itsconquest e7d0bd6b65 fix(wizard): highcontrast style for BE only options (#7543) 2022-08-24 14:48:02 +12:00
itsconquest 956a6751a2 fix(azure): correctly sort container ports [EE-4076] (#7549) 2022-08-24 12:43:06 +12:00
congs de6ef0de83 fix(stack): EE-3908 broken modal when updating/redeploying stacks (#7498) 2022-08-23 14:22:27 +12:00
fhanportainer 8f8e89af3e fix(toggle): fixed disabled toggle color in dark and high contrast modes. (#7517)
* fix(toggle): fixed disabled toggle color in dark and high contrast modes.

* fix(switch): fixed switch color in dark and high contrast modes.

* fix(switch): fixed switch in LDAP secion.

* fix(switch): corrected the blue color of Switch in dark and high contrast themes.
2022-08-23 12:11:15 +12:00
Prabhat Khera 9d45a25e99 fix minor ui issues (#7511) 2022-08-23 08:55:47 +12:00
Prabhat Khera c471b23bc5 fix(ui): minor ui issues EE-4004 (#7513) 2022-08-23 08:54:49 +12:00
fhanportainer 391d53cdc2 feat(label): uses --ui-white for control-label css class in Dark and High contrast themes (#7506)
* feat(label): uses --ui-white for control-label css class in Dark and High contrast themes.

* feat(label): uses tailwind to apply different colors in themes.

* feat(label): uses apply in control-label css class.
2022-08-23 03:56:21 +12:00
matias-portainer 3fe8697159 fix(edge): save edge checkin interval during endpoint creation EE-3958 (#7542) 2022-08-22 12:08:47 -03:00
Rex Wang 4dabe333f9 Fix(UI) fix color of file upload button in dark mode (release/2.15) EE-4009 (#7534)
* fix snapshot url parsing issue for ip addresses (#7478)

* fix(home): remove edge devices from homepage list EE-3919 (#7471)

* fix(ui/header): change font sizes [EE-3966] (#7485)

* fix(activity): fix angularjs error [EE-3968] (#7483)

* fix(activity): fix angularjs error [EE-3968] (#7482)

* fix(k8s/apps): show horizontal scrollbar [EE-3941] (#7472)

* EE-4009 fix color of file-upload button in dark mode

Co-authored-by: Matt Hook <hookenz@gmail.com>
Co-authored-by: matias-portainer <104775949+matias-portainer@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-08-22 20:25:08 +08:00
Chaim Lev-Ari 05e3634594 fix(ui): box-selector fixes [EE-3949] (#7490) 2022-08-22 11:55:51 +03:00
Ali de3c75e701 fix(kubeshell): add data-cy to buttons EE-4054 (#7537) 2022-08-22 16:52:19 +12:00
fhanportainer de5dc93b5f fix(app-template): fixed the app template list not scroll to top issue. (#7520)
* fix(app-template): fixed the app template list not scroll to top issue.

* fix(templates): added `id` prop to PageHeader component
2022-08-20 00:31:20 +12:00
fhanportainer 39896a6eaf feat(docker): fixed info icon in docker feature config section. (#7531) 2022-08-19 12:55:26 +12:00
matias-portainer 8b6bd59003 fix(home): remove edge devices from homepage list EE-3919 (#7525) 2022-08-18 10:25:19 -03:00
Rex Wang 750a37b861 EE-3998 bug fix (#7521) 2022-08-18 18:56:33 +08:00
Prabhat Khera 8a6593c7f5 fix background (#7516) 2022-08-18 22:40:01 +12:00
fhanportainer 6efd3c6a57 feat(stack): fixed stack web editor scroll bar issue. (#7532) 2022-08-18 18:00:31 +12:00
Rex Wang 8b21023c9d EE-3916 fix container link under stack detail page (#7508) 2022-08-17 23:48:15 +08:00
Rex Wang 0aebe38c31 Fix(UI) UI fixes on docker container screens (release/2.15) EE-3915 (#7499)
* EE-3915 ui fixes on docker container screens

* Update createcontainer.html

Update label
2022-08-17 23:37:31 +08:00
Chaim Lev-Ari 28c4b333ce fix(containers): make table wider [EE-3944] (#7486) 2022-08-17 12:49:25 +03:00
Ali e4c7561dfc fix(kubeconfig): update button and modal styles (#7480)
EE-3947
2022-08-17 20:00:57 +12:00
Ali 750bb9cf87 fix(k8s/apps): show horizontal scrollbar [EE-3941] (#7476) 2022-08-16 20:58:55 +03:00
Chaim Lev-Ari c5f5269366 fix(ui/header): change font sizes [EE-3966] (#7484) 2022-08-16 18:08:04 +03:00
Matt Hook b9b8d78fcc fix snapshot url parsing issue for ip addresses (#7477) 2022-08-16 10:36:01 +12:00
142 changed files with 1763 additions and 1159 deletions
+3 -3
View File
@@ -5,17 +5,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
netUrl "net/url"
"strconv" "strconv"
"time" "time"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
) )
// GetAgentVersionAndPlatform returns the agent version and platform // GetAgentVersionAndPlatform returns the agent version and platform
// //
// it sends a ping to the agent and parses the version and platform from the headers // 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) { func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{ httpCli := &http.Client{
Timeout: 3 * time.Second, Timeout: 3 * time.Second,
} }
@@ -26,7 +26,7 @@ func GetAgentVersionAndPlatform(url string, tlsConfig *tls.Config) (portainer.Ag
} }
} }
parsedURL, err := netUrl.Parse(fmt.Sprintf("%s/ping", url)) parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
@@ -259,9 +259,6 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
endpointType := portainer.DockerEnvironment endpointType := portainer.DockerEnvironment
var agentVersion string var agentVersion string
if payload.EndpointCreationType == agentEnvironment { if payload.EndpointCreationType == agentEnvironment {
payload.URL = "tcp://" + normalizeAgentAddress(payload.URL)
var tlsConfig *tls.Config var tlsConfig *tls.Config
if payload.TLS { if payload.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify) tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
@@ -105,12 +105,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
} }
if payload.URL != nil { if payload.URL != nil {
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.URL = *payload.URL
endpoint.Type == portainer.AgentOnKubernetesEnvironment {
endpoint.URL = normalizeAgentAddress(*payload.URL)
} else {
endpoint.URL = *payload.URL
}
} }
if payload.PublicURL != nil { if payload.PublicURL != nil {
-12
View File
@@ -1,18 +1,6 @@
package endpoints package endpoints
import "strings"
func BoolAddr(b bool) *bool { func BoolAddr(b bool) *bool {
boolVar := b boolVar := b
return &boolVar 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
}
+2
View File
@@ -181,6 +181,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"): case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/docker"):
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm // Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"): case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
+2 -15
View File
@@ -5,14 +5,13 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/agent" "github.com/portainer/portainer/api/http/proxy/factory/agent"
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/url"
) )
// ProxyServer provide an extended proxy with a local server to forward requests // ProxyServer provide an extended proxy with a local server to forward requests
@@ -34,7 +33,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port) urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
} }
endpointURL, err := parseURL(urlString) endpointURL, err := url.ParseURL(urlString)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL) return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
} }
@@ -99,15 +98,3 @@ func (proxy *ProxyServer) Close() {
proxy.server.Close() proxy.server.Close()
} }
} }
// parseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func parseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+3 -3
View File
@@ -5,13 +5,13 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"net/url"
"strings" "strings"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/docker" "github.com/portainer/portainer/api/http/proxy/factory/docker"
"github.com/portainer/portainer/api/internal/url"
) )
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
@@ -23,7 +23,7 @@ func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.
} }
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
endpointURL, err := url.Parse(endpoint.URL) endpointURL, err := url.ParseURL(endpoint.URL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -38,7 +38,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port) rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
} }
endpointURL, err := url.Parse(rawURL) endpointURL, err := url.ParseURL(rawURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+19
View File
@@ -0,0 +1,19 @@
package url
import (
"fmt"
"net/url"
"strings"
)
// ParseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func ParseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+5 -1
View File
@@ -52,7 +52,11 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
d.swarmStackManager.Login(registries, endpoint) d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint) defer d.swarmStackManager.Logout(endpoint)
return d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate) err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
if err != nil {
d.composeStackManager.Down(context.TODO(), stack, endpoint)
}
return err
} }
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error { func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
@@ -1,4 +1,4 @@
<button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope"> <button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn btn-light ng-scope">
<pr-icon icon="'upload'" feather="true"></pr-icon> <pr-icon icon="'upload'" feather="true"></pr-icon>
</button> </button>
<button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress"> </button> <button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn btn-sm btn-light" button-spinner="$ctrl.state.uploadInProgress"></button>
+1 -5
View File
@@ -117,10 +117,6 @@ input[type='checkbox'] {
text-align: center; text-align: center;
} }
a[ng-click] {
cursor: pointer;
}
.space-right { .space-right {
margin-right: 5px; margin-right: 5px;
} }
@@ -481,7 +477,7 @@ a[ng-click] {
:root[theme='dark'] .bootbox-checkbox-list, :root[theme='dark'] .bootbox-checkbox-list,
:root[theme='highcontrast'] .bootbox-checkbox-list { :root[theme='highcontrast'] .bootbox-checkbox-list {
background-color: var(--black-color); background-color: var(--bg-modal-content-color);
} }
.small-select { .small-select {
+13 -6
View File
@@ -12,8 +12,11 @@
} }
.control-label { .control-label {
color: var(--ui-gray-7); @apply inline-flex items-center;
font-weight: 500; @apply font-medium;
@apply text-gray-7;
@apply th-dark:text-gray-warm-3;
@apply th-highcontrast:text-white;
} }
.vertical-center { .vertical-center {
@@ -68,6 +71,10 @@
.switch input[type='checkbox']:disabled + .slider { .switch input[type='checkbox']:disabled + .slider {
background-color: var(--ui-gray-3); background-color: var(--ui-gray-3);
@apply th-dark:before:bg-gray-warm-8;
@apply th-highcontrast:before:bg-gray-warm-8;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
} }
.switch-values { .switch-values {
@@ -88,6 +95,8 @@
background-color: var(--bg-switch-box-color); background-color: var(--bg-switch-box-color);
-webkit-transition: 0.4s; -webkit-transition: 0.4s;
transition: 0.4s; transition: 0.4s;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
} }
.slider:before { .slider:before {
@@ -104,6 +113,8 @@
input:checked + .slider { input:checked + .slider {
background-color: var(--ui-blue-8); background-color: var(--ui-blue-8);
@apply th-dark:bg-blue-9;
@apply th-highcontrast:bg-blue-9;
} }
input:focus + .slider { input:focus + .slider {
@@ -355,10 +366,6 @@ input:checked + .slider:before {
color: var(--ui-error-9); color: var(--ui-error-9);
} }
.control-label {
@apply inline-flex items-center;
}
.progress { .progress {
height: 8px; height: 8px;
border-radius: 4px; border-radius: 4px;
+49 -37
View File
@@ -25,14 +25,14 @@ fieldset[disabled] .btn {
box-shadow: none; box-shadow: none;
} }
.btn-primary { .btn.btn-primary {
@apply text-white bg-blue-8 border-blue-8; @apply text-white bg-blue-8 border-blue-8;
@apply hover:text-white hover:bg-blue-9 hover:border-blue-9; @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; @apply th-dark:hover:bg-blue-7 th-dark:hover:border-blue-7;
} }
.btn-primary:active, .btn.btn-primary:active,
.btn-primary.active, .btn.btn-primary.active,
.open > .dropdown-toggle.btn-primary { .open > .dropdown-toggle.btn-primary {
@apply bg-blue-9 border-blue-5; @apply bg-blue-9 border-blue-5;
} }
@@ -44,7 +44,7 @@ fieldset[disabled] .btn {
} }
/* Button Secondary */ /* Button Secondary */
.btn-secondary { .btn.btn-secondary {
@apply border border-solid; @apply border border-solid;
@apply text-blue-9 bg-blue-2 border-blue-8; @apply text-blue-9 bg-blue-2 border-blue-8;
@@ -54,18 +54,18 @@ fieldset[disabled] .btn {
@apply th-dark:hover:bg-blue-11; @apply th-dark:hover:bg-blue-11;
} }
.btn-danger { .btn.btn-danger {
@apply bg-error-8 border-error-8; @apply bg-error-8 border-error-8;
@apply hover:bg-error-7 hover:border-error-7 hover:text-white; @apply hover:bg-error-7 hover:border-error-7 hover:text-white;
} }
.btn-danger:active, .btn.btn-danger:active,
.btn-danger.active, .btn.btn-danger.active,
.open > .dropdown-toggle.btn-danger { .open > .dropdown-toggle.btn-danger {
@apply bg-error-8 text-white border-blue-5; @apply bg-error-8 text-white border-blue-5;
} }
.btn-dangerlight { .btn.btn-dangerlight {
@apply text-error-9 th-dark:text-white; @apply text-error-9 th-dark:text-white;
@apply bg-error-3 th-dark:bg-error-9; @apply bg-error-3 th-dark:bg-error-9;
@apply hover:bg-error-2 th-dark:hover:bg-error-11; @apply hover:bg-error-2 th-dark:hover:bg-error-11;
@@ -73,39 +73,38 @@ fieldset[disabled] .btn {
@apply border border-solid; @apply border border-solid;
} }
.btn-success { .btn.btn-success {
background-color: var(--ui-success-7); background-color: var(--ui-success-7);
} }
.btn-success:hover { .btn.btn-success:hover {
color: var(--white-color); color: var(--white-color);
} }
/* secondary-grey */ /* secondary-grey */
.btn-default, .btn.btn-default,
.btn-light { .btn.btn-light {
@apply bg-white border-gray-5 text-gray-9; @apply bg-white border-gray-5 text-gray-9;
@apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10; @apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10;
/* dark mode */ /* dark mode */
@apply th-dark:bg-gray-warm-10 th-dark:border-gray-warm-7 th-dark:text-gray-warm-4; @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; @apply th-dark:hover:bg-gray-warm-9 th-dark:hover:border-gray-6 th-dark:hover:text-gray-warm-4;
@apply th-highcontrast:bg-black th-highcontrast:border-gray-2 th-highcontrast:text-white;
@apply th-highcontrast:hover:bg-gray-9 th-highcontrast:hover:border-gray-6 th-highcontrast:hover:text-gray-warm-4;
} }
.btn-light:active, .btn.btn-light:active,
.btn-light.active, .btn.btn-light.active,
.open > .dropdown-toggle.btn-light { .open > .dropdown-toggle.btn-light {
background-color: var(--ui-gray-3); background-color: var(--ui-gray-3);
} }
.hyperlink, .btn.btn-link {
.hyperlink:focus { @apply text-blue-8 hover:text-blue-9 disabled:text-gray-5;
color: var(--ui-blue-8); @apply th-dark:text-blue-8 th-dark:hover:text-blue-7;
} @apply th-highcontrast:text-blue-8 th-highcontrast:hover:text-blue-7;
.hyperlink:hover {
text-decoration: underline;
color: var(--ui-blue-9);
} }
.btn-group { .btn-group {
@@ -120,30 +119,43 @@ fieldset[disabled] .btn {
/* focus */ /* focus */
.btn-primary:focus, .btn.btn-primary:focus,
.btn-secondary:focus, .btn.btn-secondary:focus,
.btn-light:focus { .btn.btn-light:focus {
@apply border-blue-5; @apply border-blue-5;
} }
.btn-danger:focus, .btn.btn-danger:focus,
.btn-dangerlight:focus { .btn.btn-dangerlight:focus {
@apply border-blue-6; @apply border-blue-6;
} }
.btn-primary:focus, .btn.btn-primary:focus,
.btn-secondary:focus, .btn.btn-secondary:focus,
.btn-light:focus, .btn.btn-light:focus,
.btn-danger:focus, .btn.btn-danger:focus,
.btn-dangerlight:focus { .btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-3); --btn-focus-color: var(--ui-blue-3);
box-shadow: 0px 0px 0px 4px var(--btn-focus-color); box-shadow: 0px 0px 0px 4px var(--btn-focus-color);
} }
[theme='dark'] .btn-primary:focus, [theme='dark'] .btn.btn-primary:focus,
[theme='dark'] .btn-secondary:focus, [theme='dark'] .btn.btn-secondary:focus,
[theme='dark'] .btn-light:focus, [theme='dark'] .btn.btn-light:focus,
[theme='dark'] .btn-danger:focus, [theme='dark'] .btn.btn-danger:focus,
[theme='dark'] .btn-dangerlight:focus { [theme='dark'] .btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-11); --btn-focus-color: var(--ui-blue-11);
} }
a.no-link,
a[ng-click] {
@apply text-current;
@apply hover:no-underline hover:text-current;
@apply focus:no-underline focus:text-current;
}
a,
a.hyperlink {
@apply text-blue-8 hover:text-blue-9;
@apply hover:underline cursor-pointer;
}
-11
View File
@@ -31,17 +31,6 @@
border-top: 1px solid var(--border-table-top-color); border-top: 1px solid var(--border-table-top-color);
} }
a {
color: inherit;
cursor: pointer;
}
a:hover,
a:focus {
color: inherit;
text-decoration: none;
}
.input-group-addon { .input-group-addon {
color: var(--text-input-group-addon-color); color: var(--text-input-group-addon-color);
background-color: var(--bg-input-group-addon-color); background-color: var(--bg-input-group-addon-color);
+7 -3
View File
@@ -1,4 +1,8 @@
<svg width="51" height="57" viewBox="0 0 51 57" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="auto" height="auto" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M49.4176 17.75L43.882 0.819107H7.28089L1.74531 17.75C1.74531 17.75 -4.79215 34.8935 10.4787 45.4348C24.3922 55.0404 25.5814 56.2077 25.5814 56.2077C25.5814 56.2077 26.7707 55.038 40.6842 45.4348C55.955 34.8935 49.4176 17.75 49.4176 17.75Z" fill="#F4552A"/> <path
<path d="M25.5851 0.984695L31.4835 17.6745L49.4453 18.0361L35.1283 28.7097L40.3323 45.6217L25.5851 35.5293L10.838 45.6217L16.042 28.7097L1.72498 18.0361L19.6868 17.6745L25.5851 0.984695Z" fill="white"/> d="M22.9751 15.3177L20.4526 7.52911H3.77358L1.25103 15.3177C1.25103 15.3177 -1.72806 23.2041 5.23082 28.0533C11.5711 32.4721 12.1131 33.0091 12.1131 33.0091C12.1131 33.0091 12.655 32.471 18.9953 28.0533C25.9542 23.2041 22.9751 15.3177 22.9751 15.3177Z"
fill="#F4552A" />
<path
d="M12.1152 7.60529L14.803 15.283L22.9882 15.4493L16.4639 20.3594L18.8354 28.1393L12.1152 23.4966L5.39497 28.1393L7.76642 20.3594L1.24219 15.4493L9.42731 15.283L12.1152 7.60529Z"
fill="white" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 604 B

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+3 -2
View File
@@ -31,7 +31,7 @@ export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube
export const PORTAINER_FADEOUT = 1500; export const PORTAINER_FADEOUT = 1500;
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
export const BROWSER_OS_PLATFORM = navigator.userAgent.indexOf('Windows NT') > -1 ? 'win' : 'lin'; export const BROWSER_OS_PLATFORM = navigator.userAgent.indexOf('Windows') > -1 ? 'win' : navigator.userAgent.indexOf('Mac') > -1 ? 'mac' : 'lin';
export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n'; export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n';
// don't declare new constants, either: // don't declare new constants, either:
@@ -66,4 +66,5 @@ angular
.constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS) .constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS)
.constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY) .constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY)
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX) .constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX)
.constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS); .constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS)
.constant('BROWSER_OS_PLATFORM', BROWSER_OS_PLATFORM);
+12
View File
@@ -376,6 +376,17 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
}, },
}; };
var stackContainer = {
name: 'docker.stacks.stack.container',
url: '/:id?nodeName',
views: {
'content@': {
templateUrl: '~@/docker/views/containers/edit/container.html',
controller: 'ContainerController',
},
},
};
var stackCreation = { var stackCreation = {
name: 'docker.stacks.newstack', name: 'docker.stacks.newstack',
url: '/newstack', url: '/newstack',
@@ -553,6 +564,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
$stateRegistryProvider.register(serviceLogs); $stateRegistryProvider.register(serviceLogs);
$stateRegistryProvider.register(stacks); $stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack); $stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackContainer);
$stateRegistryProvider.register(stackCreation); $stateRegistryProvider.register(stackCreation);
$stateRegistryProvider.register(swarm); $stateRegistryProvider.register(swarm);
$stateRegistryProvider.register(swarmVisualizer); $stateRegistryProvider.register(swarmVisualizer);
@@ -50,9 +50,9 @@
ng-click="$ctrl.expandItem(value, !value.Expanded)" ng-click="$ctrl.expandItem(value, !value.Expanded)"
> >
<td> <td>
<a ng-if="$ctrl.itemCanExpand(value)"> <button class="btn btn-none" ng-if="$ctrl.itemCanExpand(value)" type="button">
<i ng-class="{ 'fas fa-angle-down': value.Expanded, 'fas fa-angle-right': !value.Expanded }" class="space-right" aria-hidden="true"></i> <i ng-class="{ 'fas fa-angle-down': value.Expanded, 'fas fa-angle-right': !value.Expanded }" class="space-right" aria-hidden="true"></i>
</a> </button>
<a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a> <a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a>
</td> </td>
<td>{{ value.IPAddress || '-' }}</td> <td>{{ value.IPAddress || '-' }}</td>
@@ -127,15 +127,20 @@
<thead> <thead>
<tr> <tr>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.state.open"> <th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.state.open">
<span class="md-checkbox" ng-if="!$ctrl.offlineMode"> <div class="flex gap-1 items-center">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" /> <span class="md-checkbox" ng-if="!$ctrl.offlineMode">
<label for="select_all"></label> <input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
</span> <label for="select_all"></label>
<a ng-click="$ctrl.changeOrderBy('Id')"> </span>
Id <table-column-header
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i> col-title="'Id'"
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i> can-sort="true"
</a> is-sorted="$ctrl.state.orderBy === 'Id'"
is-sorted-desc="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Id')"
></table-column-header>
</div>
<div> <div>
<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" 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 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>
@@ -160,32 +165,40 @@
</div> </div>
</th> </th>
<th> <th>
<a ng-click="$ctrl.changeOrderBy('RepoTags')"> <table-column-header
Tags col-title="'Tags'"
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && !$ctrl.state.reverseOrder"></i> can-sort="true"
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && $ctrl.state.reverseOrder"></i> is-sorted="$ctrl.state.orderBy === 'RepoTags'"
</a> is-sorted-desc="$ctrl.state.orderBy === 'RepoTags' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('RepoTags')"
></table-column-header>
</th> </th>
<th> <th>
<a ng-click="$ctrl.changeOrderBy('VirtualSize')"> <table-column-header
Size col-title="'Size'"
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && !$ctrl.state.reverseOrder"></i> can-sort="true"
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && $ctrl.state.reverseOrder"></i> is-sorted="$ctrl.state.orderBy === 'VirtualSize'"
</a> is-sorted-desc="$ctrl.state.orderBy === 'VirtualSize' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('VirtualSize')"
></table-column-header>
</th> </th>
<th> <th>
<a ng-click="$ctrl.changeOrderBy('Created')"> <table-column-header
Created col-title="'Created'"
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i> can-sort="true"
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i> is-sorted="$ctrl.state.orderBy === 'Created'"
</a> is-sorted-desc="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Created')"
></table-column-header>
</th> </th>
<th ng-if="$ctrl.showHostColumn"> <th ng-if="$ctrl.showHostColumn">
<a ng-click="$ctrl.changeOrderBy('NodeName')"> <table-column-header
Host col-title="'Host'"
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i> can-sort="true"
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i> is-sorted="$ctrl.state.orderBy === 'NodeName'"
</a> is-sorted-desc="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('NodeName')"
></table-column-header>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -156,7 +156,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event); $event.stopPropagation()" /> <input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event); $event.stopPropagation()" />
<label for="select_{{ $index }}"></label> <label for="select_{{ $index }}"></label>
</span> </span>
<a><i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i></a> <i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
</td> </td>
<td> <td>
<a ui-sref="docker.services.service({id: item.Id})" ng-click="$event.stopPropagation()">{{ item.Name }}</a> <a ui-sref="docker.services.service({id: item.Id})" ng-click="$event.stopPropagation()">{{ item.Name }}</a>
@@ -190,11 +190,17 @@
item.Id | truncate: 40 item.Id | truncate: 40
}}</a> }}</a>
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate: 40 }}</span> <span ng-if="$ctrl.offlineMode">{{ item.Id | truncate: 40 }}</span>
<btn authorization="DockerAgentBrowseList" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
<a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left"> <button
<pr-icon icon="'search'" feather="true"></pr-icon> browse ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode"
</a> type="button"
</btn> ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })"
class="btn btn-xs btn-primary space-left"
authorization="DockerAgentBrowseList"
>
<pr-icon icon="'search'" feather="true"></pr-icon> browse
</button>
<span style="margin-left: 10px" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span> <span style="margin-left: 10px" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
</td> </td>
<td>{{ item.StackName ? item.StackName : '-' }}</td> <td>{{ item.StackName ? item.StackName : '-' }}</td>
@@ -33,7 +33,7 @@
<tr> <tr>
<td> <td>
<div class="nopadding"> <div class="nopadding">
<a class="btn btn-secondary btn-sm pull-right" ng-click="$ctrl.addLabel(node)"> <pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> label </a> <a class="btn btn-secondary btn-sm pull-right" ng-click="$ctrl.addLabel(node)"> <pr-icon icon="'plus'" feather="true"></pr-icon> label </a>
</div> </div>
Node Labels Node Labels
</td> </td>
@@ -31,7 +31,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="logs_since" class="col-sm-1 control-label text-left"> Fetch </label> <label for="logs_since" class="col-sm-2 control-label text-left"> Fetch </label>
<div class="col-sm-2"> <div class="col-sm-2">
<select class="form-control" ng-model="$ctrl.sinceTimestamp" id="logs_since"> <select class="form-control" ng-model="$ctrl.sinceTimestamp" id="logs_since">
<option selected value="">All logs</option> <option selected value="">All logs</option>
@@ -40,20 +40,20 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="logs_search" class="col-sm-1 control-label text-left"> Search </label> <label for="logs_search" class="col-sm-2 control-label text-left"> Search </label>
<div class="col-sm-11"> <div class="col-sm-8">
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter..." /> <input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter..." />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="lines_count" class="col-sm-1 control-label text-left"> Lines </label> <label for="lines_count" class="col-sm-2 control-label text-left"> Lines </label>
<div class="col-sm-11"> <div class="col-sm-8">
<input class="form-control" type="number" name="lines_count" ng-model="$ctrl.lineCount" placeholder="Enter no of lines..." /> <input class="form-control" type="number" name="lines_count" ng-model="$ctrl.lineCount" placeholder="Enter no of lines..." />
</div> </div>
</div> </div>
<div class="form-group" ng-if="$ctrl.state.copySupported"> <div class="form-group" ng-if="$ctrl.state.copySupported">
<label class="col-sm-1 control-label text-left"> Actions </label> <label class="col-sm-2 control-label text-left"> Actions </label>
<div class="col-sm-11"> <div class="col-sm-10">
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0" <button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0"
><pr-icon icon="'download'" feather="true"></pr-icon> Download logs</button ><pr-icon icon="'download'" feather="true"></pr-icon> Download logs</button
> >
@@ -812,8 +812,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
function (d) { function (d) {
var containers = d; var containers = d;
$scope.runningContainers = containers; $scope.runningContainers = containers;
$scope.gpuUseAll = $scope.endpoint.Snapshots[0].GpuUseAll; $scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
$scope.gpuUseList = $scope.endpoint.Snapshots[0].GpuUseList; $scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
if ($transition$.params().from) { if ($transition$.params().from) {
loadFromContainerSpec(); loadFromContainerSpec();
} else { } else {
@@ -640,7 +640,7 @@
</div> </div>
<!-- !runtimes --> <!-- !runtimes -->
</form> </form>
<form class="form-horizontal" style="margin-top: 15px"> <form class="form-horizontal" style="margin-top: 15px" name="resourceForm">
<!-- devices --> <!-- devices -->
<div ng-if="showDeviceMapping" class="form-group"> <div ng-if="showDeviceMapping" class="form-group">
<div class="col-sm-12" style="margin-top: 5px"> <div class="col-sm-12" style="margin-top: 5px">
@@ -712,9 +712,9 @@
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }"> <div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
<div class="col-sm-12 form-section-title"> Resources </div> <div class="col-sm-12 form-section-title"> Resources </div>
<!-- memory-reservation-input --> <!-- memory-reservation-input -->
<div class="form-group"> <div class="form-group flex">
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory reservation </label> <label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left vertical-center"> Memory reservation (MB) </label>
<div class="col-sm-3"> <div class="col-sm-6">
<slider <slider
on-change="(handleResourceChange)" on-change="(handleResourceChange)"
model="formValues.MemoryReservation" model="formValues.MemoryReservation"
@@ -724,18 +724,34 @@
ng-if="state.sliderMaxMemory" ng-if="state.sliderMaxMemory"
></slider> ></slider>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2 vertical-center">
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" /> <input
name="memory_reservation"
type="number"
min="0"
max="{{ state.sliderMaxMemory }}"
class="form-control"
ng-model="formValues.MemoryReservation"
id="memory-reservation"
required
/>
</div> </div>
<div class="col-sm-4"> </div>
<p class="small text-muted mt-2"> Memory soft limit (<b>MB</b>) </p> <div class="form-group" ng-show="resourceForm.memory_reservation.$invalid">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8 small text-muted">
<div ng-messages="resourceForm.memory-reservation.$error">
<p class="vertical-center text-warning">
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Value must be between 0 and {{ state.sliderMaxMemory }}.
</p>
</div>
</div> </div>
</div> </div>
<!-- !memory-reservation-input --> <!-- !memory-reservation-input -->
<!-- memory-limit-input --> <!-- memory-limit-input -->
<div class="form-group"> <div class="form-group flex">
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory limit </label> <label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left vertical-center"> Memory limit (MB) </label>
<div class="col-sm-3"> <div class="col-sm-6">
<slider <slider
on-change="(handleResourceChange)" on-change="(handleResourceChange)"
model="formValues.MemoryLimit" model="formValues.MemoryLimit"
@@ -745,31 +761,44 @@
ng-if="state.sliderMaxMemory" ng-if="state.sliderMaxMemory"
></slider> ></slider>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2 vertical-center">
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryLimit" id="memory-limit" /> <input
name="memory_Limit"
type="number"
min="0"
max="{{ state.sliderMaxMemory }}"
class="form-control"
ng-model="formValues.MemoryLimit"
id="memory-limit"
required
/>
</div> </div>
<div class="col-sm-4"> </div>
<p class="small text-muted mt-7"> Memory limit (<b>MB</b>) </p> <div class="form-group" ng-show="resourceForm.memory_Limit.$invalid">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8 small text-muted">
<div ng-messages="resourceForm.memory-limit.$error">
<p class="vertical-center text-warning">
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Value must be between 0 and {{ state.sliderMaxMemory }}.
</p>
</div>
</div> </div>
</div> </div>
<!-- !memory-limit-input --> <!-- !memory-limit-input -->
<!-- cpu-limit-input --> <!-- cpu-limit-input -->
<div class="form-group"> <div class="form-group flex">
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> CPU limit </label> <label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left vertical-center"> Maximum CPU usage </label>
<div class="col-sm-5"> <div class="col-sm-8">
<slider <slider
on-change="(handleResourceChange)" on-change="(handleResourceChange)"
model="formValues.CpuLimit" model="formValues.CpuLimit"
floor="0" floor="0"
ceil="state.sliderMaxCpu" ceil="state.sliderMaxCpu"
step="0.25" step="0.1"
precision="2" precision="2"
ng-if="state.sliderMaxCpu" ng-if="state.sliderMaxCpu"
></slider> ></slider>
</div> </div>
<div class="col-sm-4 mt-8">
<p class="small text-muted"> Maximum CPU usage </p>
</div>
</div> </div>
<!-- !cpu-limit-input --> <!-- !cpu-limit-input -->
@@ -65,7 +65,7 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<td>ID</td> <td class="col-xs-6 col-sm-4 col-md-3 col-lg-3">ID</td>
<td>{{ container.Id }}</td> <td>{{ container.Id }}</td>
</tr> </tr>
<tr> <tr>
+7 -7
View File
@@ -77,29 +77,29 @@
</div> </div>
<div class="dashboard-grid mx-4"> <div class="dashboard-grid mx-4">
<a ui-sref="docker.stacks" ng-if="showStacks"> <a class="no-link" ui-sref="docker.stacks" ng-if="showStacks">
<dashboard-item feather-icon="true" icon="'layers'" feather-icon="true" type="'Stack'" value="stackCount"></dashboard-item> <dashboard-item feather-icon="true" icon="'layers'" type="'Stack'" value="stackCount"></dashboard-item>
</a> </a>
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="docker.services"> <a class="no-link" ui-sref="docker.services">
<dashboard-item feather-icon="true" icon="'shuffle'" type="'Service'" value="serviceCount"></dashboard-item> <dashboard-item feather-icon="true" icon="'shuffle'" type="'Service'" value="serviceCount"></dashboard-item>
</a> </a>
</div> </div>
<a ng-if="containers" ui-sref="docker.containers"> <a class="no-link" ng-if="containers" ui-sref="docker.containers">
<dashboard-item feather-icon="true" icon="'box'" 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>
<a ng-if="images" ui-sref="docker.images"> <a class="no-link" ng-if="images" ui-sref="docker.images">
<dashboard-item feather-icon="true" icon="'list'" 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>
<a ui-sref="docker.volumes"> <a class="no-link" ui-sref="docker.volumes">
<dashboard-item feather-icon="true" icon="'database'" type="'Volume'" value="volumeCount"></dashboard-item> <dashboard-item feather-icon="true" icon="'database'" type="'Volume'" value="volumeCount"></dashboard-item>
</a> </a>
<a ui-sref="docker.networks"> <a class="no-link" ui-sref="docker.networks">
<dashboard-item feather-icon="true" icon="'share2'" type="'Network'" value="networkCount"></dashboard-item> <dashboard-item feather-icon="true" icon="'share2'" type="'Network'" value="networkCount"></dashboard-item>
</a> </a>
@@ -7,8 +7,8 @@
<form class="form-horizontal" name="$ctrl.form"> <form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title"> Host and Filesystem </div> <div class="col-sm-12 form-section-title"> Host and Filesystem </div>
<div ng-if="!$ctrl.isAgent" class="form-group"> <div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small"> <span class="col-sm-12 text-muted small vertical-center">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i> <pr-icon icon="'info'" feather="true" mode="'primary'" class-name="space-right"></pr-icon>
These features are only available for an Agent enabled environments. These features are only available for an Agent enabled environments.
</span> </span>
</div> </div>
+33 -31
View File
@@ -76,37 +76,39 @@
<!-- build-method --> <!-- build-method -->
<div class="col-sm-12 form-section-title"> Build method </div> <div class="col-sm-12 form-section-title"> Build method </div>
<div class="form-group"></div> <div class="form-group"></div>
<div class="form-group" class="mb-0"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()" /> <div>
<label for="method_editor"> <input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()" />
<div class="boxselector_header vertical-center"> <label for="method_editor">
<pr-icon icon="'edit'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Web editor <pr-icon icon="'edit'" feather="true"></pr-icon>
</div> Web editor
<p>Use our Web editor</p> </div>
</label> <p>Use our Web editor</p>
</div> </label>
<div> </div>
<input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()" /> <div>
<label for="method_upload"> <input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()" />
<div class="boxselector_header vertical-center"> <label for="method_upload">
<pr-icon icon="'upload'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Upload <pr-icon icon="'upload'" feather="true"></pr-icon>
</div> Upload
<p>Upload a tarball or a Dockerfile from your computer</p> </div>
</label> <p>Upload a tarball or a Dockerfile from your computer</p>
</div> </label>
<div> </div>
<input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()" /> <div>
<label for="method_url"> <input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()" />
<div class="boxselector_header vertical-center"> <label for="method_url">
<pr-icon icon="'globe'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
URL <pr-icon icon="'globe'" feather="true"></pr-icon>
</div> URL
<p>Specify a URL to a file</p> </div>
</label> <p>Specify a URL to a file</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
+3 -1
View File
@@ -10,7 +10,9 @@
<div class="row"> <div class="row">
<div class="pull-left" ng-repeat="tag in image.RepoTags" style="display: table"> <div class="pull-left" ng-repeat="tag in image.RepoTags" style="display: table">
<div class="input-group col-md-1 !pr-3.5 !pl-3.5"> <div class="input-group col-md-1 !pr-3.5 !pl-3.5">
<span class="input-group-addon" style="border-right: 1px solid var(--border-input-group-addon-color); border-radius: 4px">{{ tag }}</span> <span class="input-group-addon" style="border-right: 1px solid var(--border-input-group-addon-color); border-radius: 4px" data-cy="image-tag-{{ tag }}">{{
tag
}}</span>
<span class="input-group-btn" style="padding: 0px 5px"> <span class="input-group-btn" style="padding: 0px 5px">
<span style="margin: 0px 5px" authorization="DockerImagePush"> <span style="margin: 0px 5px" authorization="DockerImagePush">
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Push to registry" ng-click="pushTag(tag)"> <a data-toggle="tooltip" class="btn btn-primary interactive" title="Push to registry" ng-click="pushTag(tag)">
@@ -7,7 +7,7 @@
<select class="form-control" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig"> <select class="form-control" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig">
<option selected disabled hidden value="">Select a config</option> <option selected disabled hidden value="">Select a config</option>
</select> </select>
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> add config </a> <a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'" feather="true"></pr-icon> add config </a>
</div> </div>
<table class="table" style="margin-top: 5px"> <table class="table" style="margin-top: 5px">
<thead> <thead>
@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Container labels"> <rd-widget-header icon="list" feather-icon="true" title-text="Container labels">
<div class="nopadding" authorization="DockerServiceUpdate"> <div class="nopadding" authorization="DockerServiceUpdate">
<a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating ||addContainerLabel(service)" ng-disabled="isUpdating"> <a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating ||addContainerLabel(service)" ng-disabled="isUpdating">
<pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> container label <pr-icon icon="'plus'" feather="true"></pr-icon> container label
</a> </a>
</div> </div>
</rd-widget-header> </rd-widget-header>
@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Environment variables"> <rd-widget-header icon="list" feather-icon="true" title-text="Environment variables">
<div class="nopadding" authorization="DockerServiceUpdate"> <div class="nopadding" authorization="DockerServiceUpdate">
<a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating || addEnvironmentVariable(service)" ng-disabled="isUpdating"> <a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating || addEnvironmentVariable(service)" ng-disabled="isUpdating">
<pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> environment variable <pr-icon icon="'plus'" feather="true"></pr-icon> environment variable
</a> </a>
</div> </div>
</rd-widget-header> </rd-widget-header>
@@ -10,7 +10,7 @@
<option value="none">none</option> <option value="none">none</option>
</select> </select>
<a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)"> <a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)">
<pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> add logging driver option <pr-icon icon="'plus'" feather="true"></pr-icon> add logging driver option
</a> </a>
</div> </div>
<table class="table"> <table class="table">
@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Mounts"> <rd-widget-header icon="list" feather-icon="true" title-text="Mounts">
<div class="nopadding" authorization="DockerServiceUpdate"> <div class="nopadding" authorization="DockerServiceUpdate">
<a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating ||addMount(service)" ng-disabled="isUpdating"> <a class="btn btn-secondary btn-sm pull-right" ng-click="isUpdating ||addMount(service)" ng-disabled="isUpdating">
<pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> mount <pr-icon icon="'plus'" feather="true"></pr-icon> mount
</a> </a>
</div> </div>
</rd-widget-header> </rd-widget-header>
@@ -15,7 +15,7 @@
<label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="false">Default location</label> <label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="false">Default location</label>
<label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="true">Override</label> <label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="true">Override</label>
</div> </div>
<a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)"> <pr-icon icon="'plus'" mode="'alt'" feather="true"></pr-icon> add secret </a> <a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)"> <pr-icon icon="'plus'" feather="true"></pr-icon> add secret </a>
</div> </div>
<table class="table" style="margin-top: 5px"> <table class="table" style="margin-top: 5px">
<thead> <thead>
@@ -34,27 +34,29 @@
<!-- edge-job-method-select --> <!-- edge-job-method-select -->
<div class="col-sm-12 form-section-title"> Edge job configuration </div> <div class="col-sm-12 form-section-title"> Edge job configuration </div>
<div class="form-group"></div> <div class="form-group"></div>
<div class="form-group px-4"> <div class="form-group">
<div class="boxselector_wrapper !mt-0"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper !mt-0">
<input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic" /> <div>
<label for="config_basic"> <input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic" />
<div class="boxselector_header vertical-center"> <label for="config_basic">
<pr-icon icon="'calendar'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Basic configuration <pr-icon icon="'calendar'" feather="true"></pr-icon>
</div> Basic configuration
<p>Select date from calendar</p> </div>
</label> <p>Select date from calendar</p>
</div> </label>
<div> </div>
<input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced" /> <div>
<label for="config_advanced"> <input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced" />
<div class="boxselector_header vertical-center"> <label for="config_advanced">
<pr-icon icon="'edit'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Advanced configuration <pr-icon icon="'edit'" feather="true"></pr-icon>
</div> Advanced configuration
<p>Write your own cron rule</p> </div>
</label> <p>Write your own cron rule</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -151,28 +153,29 @@
<!-- execution-method --> <!-- execution-method -->
<div ng-if="!$ctrl.model.Id"> <div ng-if="!$ctrl.model.Id">
<div class="col-sm-12 form-section-title"> Job content </div> <div class="col-sm-12 form-section-title"> Job content </div>
<div class="form-group"></div> <div class="form-group">
<div class="form-group px-4"> <div class="col-sm-12">
<div class="boxselector_wrapper !mt-0"> <div class="boxselector_wrapper">
<div> <div>
<input type="radio" id="method_editor" ng-model="$ctrl.formValues.method" value="editor" /> <input type="radio" id="method_editor" ng-model="$ctrl.formValues.method" value="editor" />
<label for="method_editor"> <label for="method_editor">
<div class="boxselector_header vertical-center"> <div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon> <pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor Web editor
</div> </div>
<p>Use our Web editor</p> <p>Use our Web editor</p>
</label> </label>
</div> </div>
<div> <div>
<input type="radio" id="method_upload" ng-model="$ctrl.formValues.method" value="upload" /> <input type="radio" id="method_upload" ng-model="$ctrl.formValues.method" value="upload" />
<label for="method_upload"> <label for="method_upload">
<div class="boxselector_header vertical-center"> <div class="boxselector_header vertical-center">
<pr-icon icon="'upload'" feather="true"></pr-icon> <pr-icon icon="'upload'" feather="true"></pr-icon>
Upload Upload
</div> </div>
<p>Upload from your computer</p> <p>Upload from your computer</p>
</label> </label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,13 +1,15 @@
import { compose, kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
export default class EdgeStackDeploymentTypeSelectorController { export default class EdgeStackDeploymentTypeSelectorController {
/* @ngInject */ /* @ngInject */
constructor() { constructor() {
this.deploymentOptions = [ this.deploymentOptions = [
{ id: 'deployment_compose', icon: 'fab fa-docker', label: 'Compose', description: 'Docker compose format', value: 0 },
{ {
id: 'deployment_kube', ...compose,
icon: 'fa fa-cubes', value: 0,
label: 'Kubernetes', },
description: 'Kubernetes manifest format', {
...kubernetes,
value: 1, value: 1,
disabled: () => { disabled: () => {
return this.hasDockerEndpoint(); return this.hasDockerEndpoint();
+46 -42
View File
@@ -26,27 +26,29 @@
</div> </div>
<div class="col-sm-12 form-section-title"> Group type </div> <div class="col-sm-12 form-section-title"> Group type </div>
<div class="col-sm-12 !px-0"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div class="boxselector"> <div class="boxselector_wrapper">
<input type="radio" id="static-group" ng-model="$ctrl.model.Dynamic" ng-value="false" ng-checked="!$ctrl.model.Dynamic" /> <div class="boxselector">
<label for="static-group"> <input type="radio" id="static-group" ng-model="$ctrl.model.Dynamic" ng-value="false" ng-checked="!$ctrl.model.Dynamic" />
<div class="boxselector_header vertical-center"> <label for="static-group">
<pr-icon icon="'list'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Static <pr-icon icon="'list'" feather="true"></pr-icon>
</div> Static
<p>Manually select Edge environments</p> </div>
</label> <p>Manually select Edge environments</p>
</div> </label>
<div class="boxselector"> </div>
<input type="radio" id="dynamic-group" ng-model="$ctrl.model.Dynamic" ng-value="true" ng-checked="$ctrl.model.Dynamic" /> <div class="boxselector">
<label for="dynamic-group"> <input type="radio" id="dynamic-group" ng-model="$ctrl.model.Dynamic" ng-value="true" ng-checked="$ctrl.model.Dynamic" />
<div class="boxselector_header vertical-center"> <label for="dynamic-group">
<pr-icon icon="'tag'" feather="true" className="'feather'"></pr-icon> <div class="boxselector_header vertical-center">
Dynamic <pr-icon icon="'tag'" feather="true"></pr-icon>
</div> Dynamic
<p>Automatically associate environments via tags</p> </div>
</label> <p>Automatically associate environments via tags</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -78,27 +80,29 @@
<!-- DynamicGroup --> <!-- DynamicGroup -->
<div ng-if="$ctrl.model.Dynamic"> <div ng-if="$ctrl.model.Dynamic">
<div class="col-sm-12 form-section-title"> Tags </div> <div class="col-sm-12 form-section-title"> Tags </div>
<div ng-if="$ctrl.tags.length" class="form-group col-sm-12"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div class="boxselector"> <div class="boxselector_wrapper">
<input type="radio" id="or-selector" ng-model="$ctrl.model.PartialMatch" ng-value="true" ng-checked="$ctrl.model.PartialMatch" /> <div class="boxselector">
<label for="or-selector"> <input type="radio" id="or-selector" ng-model="$ctrl.model.PartialMatch" ng-value="true" ng-checked="$ctrl.model.PartialMatch" />
<div class="boxselector_header vertical-center"> <label for="or-selector">
<pr-icon icon="'tag'" feather="true"></pr-icon> <div class="boxselector_header vertical-center">
Partial match <pr-icon icon="'tag'" feather="true"></pr-icon>
</div> Partial match
<p>Associate any environment matching at least one of the selected tags</p> </div>
</label> <p>Associate any environment matching at least one of the selected tags</p>
</div> </label>
<div class="boxselector"> </div>
<input type="radio" id="and-selector" ng-model="$ctrl.model.PartialMatch" ng-value="false" ng-checked="!$ctrl.model.PartialMatch" /> <div class="boxselector">
<label for="and-selector"> <input type="radio" id="and-selector" ng-model="$ctrl.model.PartialMatch" ng-value="false" ng-checked="!$ctrl.model.PartialMatch" />
<div class="boxselector_header vertical-center"> <label for="and-selector">
<pr-icon icon="'tag'" feather="true" className="'feather'"></pr-icon> <div class="boxselector_header vertical-center">
Full match <pr-icon icon="'tag'" feather="true"></pr-icon>
</div> Full match
<p>Associate any environment matching all of the selected tags</p> </div>
</label> <p>Associate any environment matching all of the selected tags</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,14 +1,11 @@
import { editor, git, template, upload } from '@@/BoxSelector/common-options/build-methods';
class DockerComposeFormController { class DockerComposeFormController {
/* @ngInject */ /* @ngInject */
constructor($async, EdgeTemplateService, Notifications) { constructor($async, EdgeTemplateService, Notifications) {
Object.assign(this, { $async, EdgeTemplateService, Notifications }); Object.assign(this, { $async, EdgeTemplateService, Notifications });
this.methodOptions = [ this.methodOptions = [editor, upload, git, template];
{ id: 'method_editor', icon: 'edit', featherIcon: true, label: 'Web editor', description: 'Use our Web editor', value: 'editor' },
{ id: 'method_upload', icon: 'upload', featherIcon: true, label: 'Upload', description: 'Upload from your computer', value: 'upload' },
{ id: 'method_repository', icon: 'github', featherIcon: true, label: 'Repository', description: 'Use a git repository', value: 'repository' },
{ id: 'method_template', icon: 'file-text', featherIcon: true, label: 'Template', description: 'Use an Edge stack template', value: 'template' },
];
this.selectedTemplate = null; this.selectedTemplate = null;
@@ -1,13 +1,11 @@
import { editor, git, upload } from '@@/BoxSelector/common-options/build-methods';
class KubeManifestFormController { class KubeManifestFormController {
/* @ngInject */ /* @ngInject */
constructor($async) { constructor($async) {
Object.assign(this, { $async }); Object.assign(this, { $async });
this.methodOptions = [ this.methodOptions = [editor, upload, git];
{ id: 'method_editor', icon: 'edit', featherIcon: true, label: 'Web editor', description: 'Use our Web editor', value: 'editor' },
{ id: 'method_upload', icon: 'upload', featherIcon: true, label: 'Upload', description: 'Upload from your computer', value: 'upload' },
{ id: 'method_repository', icon: 'github', featherIcon: true, label: 'Repository', description: 'Use a git repository', value: 'repository' },
];
this.onChangeFileContent = this.onChangeFileContent.bind(this); this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this); this.onChangeFormValues = this.onChangeFormValues.bind(this);
@@ -113,7 +113,7 @@
</div> </div>
</div> </div>
<!-- data table content --> <!-- data table content -->
<div class="inner-datatable"> <div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
<table class="table table-hover table-filters nowrap-cells" data-cy="k8sApp-appTable"> <table class="table table-hover table-filters nowrap-cells" data-cy="k8sApp-appTable">
<thead ng-class="{ 'secondary-heading': !$ctrl.isPrimary }"> <thead ng-class="{ 'secondary-heading': !$ctrl.isPrimary }">
<tr role="row"> <tr role="row">
@@ -238,13 +238,18 @@
</div> </div>
</td> </td>
<td> <td>
<a ng-if="item.KubernetesApplications" ui-sref="kubernetes.helm({ name: item.Name, namespace: item.ResourcePool })" ng-click="$event.stopPropagation()" <a
ng-if="item.KubernetesApplications"
ui-sref="kubernetes.helm({ name: item.Name, namespace: item.ResourcePool })"
ng-click="$event.stopPropagation()"
class="hyperlink"
>{{ item.Name }} >{{ item.Name }}
</a> </a>
<a <a
ng-if="!item.KubernetesApplications" ng-if="!item.KubernetesApplications"
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })" ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })"
ng-click="$event.stopPropagation()" ng-click="$event.stopPropagation()"
class="hyperlink"
>{{ item.Name }} >{{ item.Name }}
</a> </a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span> <span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
@@ -196,7 +196,7 @@
<div class="form-group !mx-0 !pl-0 col-sm-3 clear-both" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType"> <div class="form-group !mx-0 !pl-0 col-sm-3 clear-both" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-addon">Route</span> <span class="input-group-addon required">Route</span>
<input <input
class="form-control" class="form-control"
name="ingress_route_{{ $index }}" name="ingress_route_{{ $index }}"
@@ -3,14 +3,12 @@
<div class="form-group" ng-if="$ctrl.isCreation"> <div class="form-group" ng-if="$ctrl.isCreation">
<div class="col-sm-12"> <div class="col-sm-12">
<p> <button type="button" class="btn btn-link btn-sm hover:no-underline !ml-0 p-0" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.showAdvancedMode()">
<a class="small interactive vertical-center" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.showAdvancedMode()"> <pr-icon icon="'list'" feather="true"></pr-icon> Advanced mode
<pr-icon icon="'list'" feather="true"></pr-icon> Advanced mode </button>
</a> <button type="button" class="btn btn-link btn-sm hover:no-underline !ml-0 p-0" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.showSimpleMode()">
<a class="small interactive vertical-center" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.showSimpleMode()"> <pr-icon icon="'edit'" feather="true"></pr-icon> Simple mode
<pr-icon icon="'edit'" feather="true"></pr-icon> Simple mode </button>
</a>
</p>
</div> </div>
<div class="col-sm-12 small text-muted vertical-center" ng-if="$ctrl.formValues.IsSimple"> <div class="col-sm-12 small text-muted vertical-center" ng-if="$ctrl.formValues.IsSimple">
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon> <pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
@@ -1,17 +1,14 @@
import { buildOption } from '@/portainer/components/BoxSelector';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils'; import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service'; import { isBE } from '@/portainer/feature-flags/feature-flags.service';
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
class KubeCreateCustomTemplateViewController { class KubeCreateCustomTemplateViewController {
/* @ngInject */ /* @ngInject */
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService) { constructor($async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService }); Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService });
this.methodOptions = [ this.methodOptions = [editor, upload];
buildOption('method_editor', 'svg-custom', 'Web editor', 'Use our Web editor', 'editor'),
buildOption('method_upload', 'svg-upload', 'Upload', 'Upload from your computer', 'upload'),
];
this.templates = null; this.templates = null;
this.isTemplateVariablesEnabled = isBE; this.isTemplateVariablesEnabled = isBE;
@@ -145,7 +145,7 @@
</div> </div>
<div class="form-group" ng-show="kubernetesApplicationCreationForm.application_name.$invalid || ctrl.state.alreadyExists"> <div class="form-group" ng-show="kubernetesApplicationCreationForm.application_name.$invalid || ctrl.state.alreadyExists">
<div class="small"> <div class="small">
<div class="col-sm-3 col-lg-2"></div> <div class="col-sm-3 col-lg-2">&nbsp;</div>
<div class="col-sm-8" ng-messages="kubernetesApplicationCreationForm.application_name.$error"> <div class="col-sm-8" ng-messages="kubernetesApplicationCreationForm.application_name.$error">
<p class="text-muted vertical-center" ng-message="required" <p class="text-muted vertical-center" ng-message="required"
><pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p ><pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field is required.</p
@@ -156,10 +156,12 @@
alphanumeric character (e.g. 'my-name', or 'abc-123'). alphanumeric character (e.g. 'my-name', or 'abc-123').
</p> </p>
</div> </div>
<p class="text-muted vertical-center" ng-if="ctrl.state.alreadyExists"> <div class="col-sm-8" ng-if="ctrl.state.alreadyExists">
<pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> <p class="text-muted vertical-center">
An application with the same name already exists inside the selected namespace. <pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
</p> An application with the same name already exists inside the selected namespace.
</p>
</div>
</div> </div>
</div> </div>
<!-- #endregion --> <!-- #endregion -->
@@ -709,83 +711,85 @@
</div> </div>
<!-- access policy options --> <!-- access policy options -->
<div class="form-group" style="margin-bottom: 0"> <div class="form-group">
<div class="boxselector_wrapper !px-[15px]"> <div class="col-sm-12">
<div <div class="boxselector_wrapper">
ng-if=" <div
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) || ng-if="
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED) (!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
" (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)
> "
<input
type="radio"
id="data_access_isolated"
ng-value="ctrl.ApplicationDataAccessPolicies.ISOLATED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_isolated">
<div class="boxselector_header">
<pr-icon icon="'svg-cubes'"></pr-icon>
Isolated
</div>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div
style="color: #767676"
ng-if="
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes
"
>
<input type="radio" id="data_access_isolated" disabled />
<label
for="data_access_isolated"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
> >
<div class="boxselector_header"> <input
<pr-icon icon="'svg-cubes'"></pr-icon> type="radio"
Isolated id="data_access_isolated"
</div> ng-value="ctrl.ApplicationDataAccessPolicies.ISOLATED"
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p> ng-model="ctrl.formValues.DataAccessPolicy"
</label> ng-change="ctrl.resetDeploymentType()"
</div> />
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)"> <label for="data_access_isolated">
<input <div class="boxselector_header">
type="radio" <pr-icon icon="'svg-cubes'"></pr-icon>
id="data_access_shared" Isolated
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED" </div>
ng-model="ctrl.formValues.DataAccessPolicy" <p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
ng-change="ctrl.resetDeploymentType()" </label>
/> </div>
<label for="data_access_shared"> <div
<div class="boxselector_header"> style="color: #767676"
<pr-icon icon="'box'" feather="true"></pr-icon> ng-if="
Shared (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes
</div> "
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
<div style="color: #767676" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<input type="radio" id="data_access_shared" disabled />
<label
for="data_access_shared"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
> >
<div class="boxselector_header"> <input type="radio" id="data_access_isolated" disabled />
<pr-icon icon="'sliders'" feather="true"></pr-icon> <label
Shared for="data_access_isolated"
</div> tooltip-append-to-body="true"
<p>Application will be deployed as a Deployment with a shared storage access</p> tooltip-placement="bottom"
</label> tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
>
<div class="boxselector_header">
<pr-icon icon="'svg-cubes'"></pr-icon>
Isolated
</div>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)">
<input
type="radio"
id="data_access_shared"
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_shared">
<div class="boxselector_header">
<pr-icon icon="'box'" feather="true"></pr-icon>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
<div style="color: #767676" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<input type="radio" id="data_access_shared" disabled />
<label
for="data_access_shared"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
>
<div class="boxselector_header">
<pr-icon icon="'sliders'" feather="true"></pr-icon>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -897,56 +901,58 @@
</div> </div>
<!-- deployment options --> <!-- deployment options -->
<div class="form-group" style="margin-bottom: 0"> <div class="form-group">
<div class="boxselector_wrapper !px-[15px]"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input <div>
type="radio" <input
id="deployment_replicated" type="radio"
ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED" id="deployment_replicated"
ng-model="ctrl.formValues.DeploymentType" ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED"
data-cy="k8sAppCreate-replicatedDeploymentButton" ng-model="ctrl.formValues.DeploymentType"
/> data-cy="k8sAppCreate-replicatedDeploymentButton"
<label for="deployment_replicated"> />
<div class="boxselector_header"> <label for="deployment_replicated">
<pr-icon icon="'sliders'" feather="true"></pr-icon> <div class="boxselector_header">
Replicated <pr-icon icon="'sliders'" feather="true"></pr-icon>
</div> Replicated
<p>Run one or multiple instances of this container</p> </div>
</label> <p>Run one or multiple instances of this container</p>
</div> </label>
<div ng-if="!ctrl.supportGlobalDeployment()"> </div>
<input type="radio" id="deployment_global" disabled /> <div ng-if="!ctrl.supportGlobalDeployment()">
<label <input type="radio" id="deployment_global" disabled />
for="deployment_global" <label
tooltip-append-to-body="true" for="deployment_global"
tooltip-placement="bottom" tooltip-append-to-body="true"
tooltip-class="portainer-tooltip" tooltip-placement="bottom"
uib-tooltip="The storage or access policy used for persisted folders cannot be used with this option" tooltip-class="portainer-tooltip"
> uib-tooltip="The storage or access policy used for persisted folders cannot be used with this option"
<div class="boxselector_header"> >
<pr-icon icon="'svg-cubes'"></pr-icon> <div class="boxselector_header">
Global <pr-icon icon="'svg-cubes'"></pr-icon>
</div> Global
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p> </div>
</label> <p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</div> </label>
<div ng-if="ctrl.supportGlobalDeployment()"> </div>
<input <div ng-if="ctrl.supportGlobalDeployment()">
type="radio" <input
id="deployment_global" type="radio"
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL" id="deployment_global"
ng-model="ctrl.formValues.DeploymentType" ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-click="ctrl.unselectAutoScaler()" ng-model="ctrl.formValues.DeploymentType"
data-cy="k8sAppCreate-globalDeployButton" ng-click="ctrl.unselectAutoScaler()"
/> data-cy="k8sAppCreate-globalDeployButton"
<label for="deployment_global"> />
<div class="boxselector_header"> <label for="deployment_global">
<pr-icon icon="'svg-cubes'"></pr-icon> <div class="boxselector_header">
Global <pr-icon icon="'svg-cubes'"></pr-icon>
</div> Global
<p>Application will be deployed as a DaemonSet with an instance on each node of the sdfh</p> </div>
</label> <p>Application will be deployed as a DaemonSet with an instance on each node of the sdfh</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1055,90 +1061,96 @@
</div> </div>
<div class="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed"> <div class="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed">
<table class="table" style="margin-bottom: 0px"> <div class="row">
<tbody> <div class="col-sm-4 pl-0">
<tr class="small"> <label class="control-label text-left pb-2" for="auto_scaler_min">Minimum instances</label>
<td style="width: 33%; border: none; padding: 2px 0 2px 0">Minimum instances</td> <div class="input-group input-group-sm" style="width: 100%">
<td style="width: 33%; border: none; padding: 2px 0 2px 0">Maximum instances</td> <input
<td style="width: 33%; border: none; padding: 2px 0 2px 0"> type="number"
Target CPU usage (<b>%</b>) class="form-control"
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'"> name="auto_scaler_min"
</portainer-tooltip> min="0"
</td> ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
</tr> ng-model="ctrl.formValues.AutoScaler.MinReplicas"
<tr> data-cy="k8sAppCreate-autoScaleMin"
<td style="padding: 8px 5px 5px 0; border: none"> required
<div class="input-group input-group-sm" style="width: 100%"> />
<input </div>
type="number" <span ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid">
class="form-control" <div class="small text-muted" style="margin-top: 5px">
name="auto_scaler_min" <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error">
min="0" <p ng-message="required" class="vertical-center">
ng-max="ctrl.formValues.AutoScaler.MaxReplicas" <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances is required.
ng-model="ctrl.formValues.AutoScaler.MinReplicas" </p>
data-cy="k8sAppCreate-autoScaleMin" <p ng-message="min" class="vertical-center">
required <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be greater than 0.
/> </p>
</div> <p ng-message="max" class="vertical-center">
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be smaller than maximum instances.
<div class="small text-warning" style="margin-top: 5px"> </p>
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error"> </ng-messages>
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances is required.</p> </div>
<p ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be greater than 0.</p> </span>
<p ng-message="max" </div>
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be smaller than maximum instances.</p <div class="col-sm-4 pl-0">
> <label class="control-label text-left pb-2" for="auto_scaler_max">Maximum instances</label>
</ng-messages> <div class="input-group input-group-sm" style="width: 100%">
</div> <input
</div> type="number"
</td> class="form-control"
<td style="padding: 8px 5px 5px 0; border: none"> name="auto_scaler_max"
<div class="input-group input-group-sm" style="width: 100%"> ng-min="ctrl.formValues.AutoScaler.MinReplicas"
<input ng-model="ctrl.formValues.AutoScaler.MaxReplicas"
type="number" />
class="form-control" </div>
name="auto_scaler_max" <span ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()">
ng-min="ctrl.formValues.AutoScaler.MinReplicas" <div class="small text-muted" style="margin-top: 5px">
ng-model="ctrl.formValues.AutoScaler.MaxReplicas" <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error">
/> <p ng-message="required" class="vertical-center">
</div> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances is required.
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()"> </p>
<div class="small text-warning" style="margin-top: 5px"> <p ng-message="min" class="vertical-center">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error"> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances must be greater than minimum instances.
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances is required.</p> </p>
<p ng-message="min" </ng-messages>
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances must be greater than minimum instances.</p </div>
> </span>
</ng-messages> </div>
</div> <div class="col-sm-4 pl-0">
</div> <label class="control-label text-left pb-2" for="auto_scaler_cpu">
</td> Target CPU usage (<b>%</b>)
<td style="padding: 8px 5px 5px 0; border: none"> <portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
<div class="input-group input-group-sm" style="width: 100%"> </portainer-tooltip>
<input </label>
type="number" <div class="input-group input-group-sm" style="width: 100%">
class="form-control" <input
name="auto_scaler_cpu" type="number"
ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization" class="form-control"
min="1" name="auto_scaler_cpu"
max="100" ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization"
required min="1"
data-cy="k8sAppCreate-targetCPUInput" max="100"
/> required
</div> data-cy="k8sAppCreate-targetCPUInput"
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid"> />
<div class="small text-warning" style="margin-top: 5px"> </div>
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error"> <span ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage is required.</p> <div class="small text-muted" style="margin-top: 5px">
<p ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be greater than 0.</p> <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error">
<p ng-message="max"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be smaller than 100.</p> <p ng-message="required" class="vertical-center">
</ng-messages> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage is required.
</div> </p>
</div> <p ng-message="min" class="vertical-center">
</td> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be greater than 0.
</tr> </p>
</tbody> <p ng-message="max" class="vertical-center">
</table> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be smaller than 100.
</p>
</ng-messages>
</div>
</span>
</div>
</div>
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px"> <div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px">
<div class="col-sm-12 small text-danger"> <div class="col-sm-12 small text-danger">
@@ -1234,39 +1246,41 @@
</div> </div>
<!-- placement policy options --> <!-- placement policy options -->
<div class="form-group" style="margin-bottom: 0" ng-if="ctrl.formValues.Placements.length"> <div class="form-group" ng-if="ctrl.formValues.Placements.length">
<div class="boxselector_wrapper !px-[15px]"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input <div>
type="radio" <input
id="placement_hard" type="radio"
ng-value="ctrl.ApplicationPlacementTypes.MANDATORY" id="placement_hard"
ng-model="ctrl.formValues.PlacementType" ng-value="ctrl.ApplicationPlacementTypes.MANDATORY"
data-cy="k8sAppCreate-mandatoryPlacementButton" ng-model="ctrl.formValues.PlacementType"
/> data-cy="k8sAppCreate-mandatoryPlacementButton"
<label for="placement_hard"> />
<div class="boxselector_header"> <label for="placement_hard">
<pr-icon icon="'sliders'" feather="true"></pr-icon> <div class="boxselector_header">
Mandatory <pr-icon icon="'sliders'" feather="true"></pr-icon>
</div> Mandatory
<p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p> </div>
</label> <p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p>
</div> </label>
<div> </div>
<input <div>
type="radio" <input
id="placement_soft" type="radio"
ng-value="ctrl.ApplicationPlacementTypes.PREFERRED" id="placement_soft"
ng-model="ctrl.formValues.PlacementType" ng-value="ctrl.ApplicationPlacementTypes.PREFERRED"
data-cy="k8sAppCreate-prefferedPlacementButton" ng-model="ctrl.formValues.PlacementType"
/> data-cy="k8sAppCreate-prefferedPlacementButton"
<label for="placement_soft"> />
<div class="boxselector_header"> <label for="placement_soft">
<pr-icon icon="'align-justify'" feather="true"></pr-icon> <div class="boxselector_header">
Preferred <pr-icon icon="'align-justify'" feather="true"></pr-icon>
</div> Preferred
<p>Schedule this application on nodes that match the rules if possible</p> </div>
</label> <p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -87,27 +87,29 @@
</div> </div>
<!-- type options --> <!-- type options -->
<div class="form-group px-[15px]" style="margin-bottom: 0"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" /> <div>
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton"> <input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
<div class="boxselector_header"> <label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<pr-icon icon="'svg-filecode'"></pr-icon> <div class="boxselector_header">
ConfigMap <pr-icon icon="'svg-filecode'"></pr-icon>
</div> ConfigMap
<p>This configuration holds non-sensitive information</p> </div>
</label> <p>This configuration holds non-sensitive information</p>
</div> </label>
<div> </div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" /> <div>
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton"> <input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
<div class="boxselector_header"> <label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<pr-icon icon="'lock'" feather="true"></pr-icon> <div class="boxselector_header">
Secret <pr-icon icon="'lock'" feather="true"></pr-icon>
</div> Secret
<p>This configuration holds sensitive information</p> </div>
</label> <p>This configuration holds sensitive information</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -40,23 +40,23 @@
<div class="dashboard-grid mx-4"> <div class="dashboard-grid mx-4">
<div ng-if="ctrl.pools" data-cy="k8sDashboard-namespaces"> <div ng-if="ctrl.pools" data-cy="k8sDashboard-namespaces">
<a ui-sref="kubernetes.resourcePools"> <a class="no-link" ui-sref="kubernetes.resourcePools">
<dashboard-item feather-icon="true" icon="'layers'" type="'Namespace'" value="ctrl.pools.length"></dashboard-item> <dashboard-item feather-icon="true" icon="'layers'" type="'Namespace'" value="ctrl.pools.length"></dashboard-item>
</a> </a>
</div> </div>
<div ng-if="ctrl.applications" data-cy="k8sDashboard-applications"> <div ng-if="ctrl.applications" data-cy="k8sDashboard-applications">
<a ui-sref="kubernetes.applications"> <a class="no-link" ui-sref="kubernetes.applications">
<dashboard-item feather-icon="true" icon="'box'" type="'Application'" value="ctrl.applications.length"></dashboard-item> <dashboard-item feather-icon="true" icon="'box'" type="'Application'" value="ctrl.applications.length"></dashboard-item>
</a> </a>
</div> </div>
<div ng-if="ctrl.configurations" data-cy="k8sDashboard-configurations"> <div ng-if="ctrl.configurations" data-cy="k8sDashboard-configurations">
<a ui-sref="kubernetes.configurations"> <a class="no-link" ui-sref="kubernetes.configurations">
<dashboard-item feather-icon="true" icon="'lock'" type="'ConfigMaps & Secret'" value="ctrl.configurations.length"></dashboard-item> <dashboard-item feather-icon="true" icon="'lock'" type="'ConfigMaps & Secret'" value="ctrl.configurations.length"></dashboard-item>
</a> </a>
</div> </div>
<div ng-if="ctrl.volumes" data-cy="k8sDashboard-volumes"> <div ng-if="ctrl.volumes" data-cy="k8sDashboard-volumes">
<a ui-sref="kubernetes.volumes"> <a class="no-link" ui-sref="kubernetes.volumes">
<dashboard-item feather-icon="true" icon="'database'" type="'Volume'" value="ctrl.volumes.length"></dashboard-item> <dashboard-item feather-icon="true" icon="'database'" type="'Volume'" value="ctrl.volumes.length"></dashboard-item>
</a> </a>
</div> </div>
@@ -5,9 +5,10 @@ import uuidv4 from 'uuid/v4';
import PortainerError from '@/portainer/error'; import PortainerError from '@/portainer/error';
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { buildOption } from '@/portainer/components/BoxSelector';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils'; import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service'; import { isBE } from '@/portainer/feature-flags/feature-flags.service';
import { compose, kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
import { editor, git, template, url } from '@@/BoxSelector/common-options/build-methods';
class KubernetesDeployController { class KubernetesDeployController {
/* @ngInject */ /* @ngInject */
@@ -27,15 +28,15 @@ class KubernetesDeployController {
this.isTemplateVariablesEnabled = isBE; this.isTemplateVariablesEnabled = isBE;
this.deployOptions = [ this.deployOptions = [
buildOption('method_kubernetes', 'svg-kubernetes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES), { ...kubernetes, value: KubernetesDeployManifestTypes.KUBERNETES },
buildOption('method_compose', 'svg-dockercompose', 'Compose', 'Docker compose format', KubernetesDeployManifestTypes.COMPOSE), { ...compose, value: KubernetesDeployManifestTypes.COMPOSE },
]; ];
this.methodOptions = [ this.methodOptions = [
buildOption('method_repo', 'svg-git', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT), { ...git, value: KubernetesDeployBuildMethods.GIT },
buildOption('method_editor', 'svg-custom', 'Web editor', 'Use our Web editor', KubernetesDeployBuildMethods.WEB_EDITOR), { ...editor, value: KubernetesDeployBuildMethods.WEB_EDITOR },
buildOption('method_url', 'svg-url', 'URL', 'Specify a URL to a file', KubernetesDeployBuildMethods.URL), { ...url, value: KubernetesDeployBuildMethods.URL },
buildOption('method_template', 'svg-template', 'Custom Template', 'Use a custom template', KubernetesDeployBuildMethods.CUSTOM_TEMPLATE), { ...template, value: KubernetesDeployBuildMethods.CUSTOM_TEMPLATE },
]; ];
this.state = { this.state = {
@@ -42,6 +42,9 @@
<p class="vertical-center" ng-if="$ctrl.state.hasPrefixKube" <p class="vertical-center" ng-if="$ctrl.state.hasPrefixKube"
><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Prefix "kube-" is reserved for Kubernetes system namespaces.</p ><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Prefix "kube-" is reserved for Kubernetes system namespaces.</p
> >
<p class="vertical-center" ng-if="$ctrl.state.isAlreadyExist">
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> A namespace with the same name already exists.
</p>
</div> </div>
</div> </div>
</span> </span>
@@ -47,16 +47,12 @@ export function EditDetails({
return ( return (
<> <>
<div className="form-group"> <BoxSelector
<div className="col-sm-12"> radioName={withNamespace('ownership')}
<BoxSelector value={values.ownership}
radioName={withNamespace('ownership')} options={options}
value={values.ownership} onChange={(ownership) => handleChangeOwnership(ownership)}
options={options} />
onChange={(ownership) => handleChangeOwnership(ownership)}
/>
</div>
</div>
{values.ownership === ResourceControlOwnership.RESTRICTED && ( {values.ownership === ResourceControlOwnership.RESTRICTED && (
<div aria-label="extra-options"> <div aria-label="extra-options">
@@ -6,6 +6,7 @@ import { ownershipIcon } from '@/portainer/filters/filters';
import { Team } from '@/portainer/teams/types'; import { Team } from '@/portainer/teams/types';
import { BoxSelectorOption } from '@@/BoxSelector/types'; import { BoxSelectorOption } from '@@/BoxSelector/types';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
import { ResourceControlOwnership } from '../types'; import { ResourceControlOwnership } from '../types';
@@ -15,7 +16,7 @@ const publicOption: BoxSelectorOption<ResourceControlOwnership> = {
id: 'access_public', id: 'access_public',
description: description:
'I want any user with access to this environment to be able to manage this resource', 'I want any user with access to this environment to be able to manage this resource',
icon: ownershipIcon('public'), icon: <BadgeIcon icon={ownershipIcon('public')} />,
}; };
export function useOptions( export function useOptions(
@@ -40,14 +41,14 @@ function adminOptions() {
return [ return [
buildOption( buildOption(
'access_administrators', 'access_administrators',
ownershipIcon('administrators'), <BadgeIcon icon={ownershipIcon('administrators')} />,
'Administrators', 'Administrators',
'I want to restrict the management of this resource to administrators only', 'I want to restrict the management of this resource to administrators only',
ResourceControlOwnership.ADMINISTRATORS ResourceControlOwnership.ADMINISTRATORS
), ),
buildOption( buildOption(
'access_restricted', 'access_restricted',
ownershipIcon('restricted'), <BadgeIcon icon={ownershipIcon('restricted')} />,
'Restricted', 'Restricted',
'I want to restrict the management of this resource to a set of users and/or teams', 'I want to restrict the management of this resource to a set of users and/or teams',
ResourceControlOwnership.RESTRICTED ResourceControlOwnership.RESTRICTED
@@ -58,7 +59,7 @@ function nonAdminOptions(teams?: Team[]) {
return _.compact([ return _.compact([
buildOption( buildOption(
'access_private', 'access_private',
ownershipIcon('private'), <BadgeIcon icon={ownershipIcon('private')} />,
'Private', 'Private',
'I want to this resource to be manageable by myself only', 'I want to this resource to be manageable by myself only',
ResourceControlOwnership.PRIVATE ResourceControlOwnership.PRIVATE
@@ -67,7 +68,7 @@ function nonAdminOptions(teams?: Team[]) {
teams.length > 0 && teams.length > 0 &&
buildOption( buildOption(
'access_restricted', 'access_restricted',
ownershipIcon('restricted'), <BadgeIcon icon={ownershipIcon('restricted')} />,
'Restricted', 'Restricted',
teams.length === 1 teams.length === 1
? `I want any member of my team (${teams[0].Name}) to be able to manage this resource` ? `I want any member of my team (${teams[0].Name}) to be able to manage this resource`
@@ -1,15 +1,16 @@
import { FeatureId } from '@/portainer/feature-flags/enums'; import { FeatureId } from '@/portainer/feature-flags/enums';
import { BoxSelectorOption } from '@@/BoxSelector/types'; import { BoxSelectorOption } from '@@/BoxSelector/types';
import { IconProps } from '@@/Icon';
export function buildOption<T extends number | string>( export function buildOption<T extends number | string>(
id: string, id: string,
icon: string, icon: IconProps['icon'],
label: string, label: string,
description: string, description: string,
value: T, value: T,
feature?: FeatureId, feature?: FeatureId,
featherIcon?: boolean featherIcon?: IconProps['featherIcon']
): BoxSelectorOption<T> { ): BoxSelectorOption<T> {
return { id, icon, label, description, value, feature, featherIcon }; return { id, icon, label, description, value, feature, featherIcon };
} }
@@ -15,51 +15,54 @@
</div> </div>
<!-- !access-control-switch --> <!-- !access-control-switch -->
<!-- restricted-access --> <!-- restricted-access -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled" style="margin-bottom: 0"> <div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled">
<div class="boxselector_wrapper px-[15px]"> <div class="col-sm-12">
<div ng-if="$ctrl.isAdmin"> <div class="boxselector_wrapper">
<input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators" /> <div ng-if="$ctrl.isAdmin">
<label for="access_administrators" data-cy="portainer-selectAdminAccess"> <input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators" />
<div class="boxselector_header"> <label for="access_administrators" data-cy="portainer-selectAdminAccess">
<pr-icon icon="'eye-off'" feather="true"></pr-icon> <div class="boxselector_header">
Administrators <pr-icon icon="'eye-off'" feather="true"></pr-icon>
</div> Administrators
<p class="boxselector_content">I want to restrict the management of this resource to administrators only</p> </div>
</label> <p class="boxselector_content">I want to restrict the management of this resource to administrators only</p>
</div> </label>
<div ng-if="$ctrl.isAdmin"> </div>
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" /> <div ng-if="$ctrl.isAdmin">
<label for="access_restricted" data-cy="portainer-selectRestrictedAccess"> <input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<div class="boxselector_header"> <label for="access_restricted" data-cy="portainer-selectRestrictedAccess">
<pr-icon icon="'users'" feather="true"></pr-icon> <div class="boxselector_header">
Restricted <pr-icon icon="'users'" feather="true"></pr-icon>
</div> Restricted
<p class="boxselector_content"> I want to restrict the management of this resource to a set of users and/or teams </p> </div>
</label> <p class="boxselector_content"> I want to restrict the management of this resource to a set of users and/or teams </p>
</div> </label>
<div ng-if="!$ctrl.isAdmin"> </div>
<input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private" /> <div ng-if="!$ctrl.isAdmin">
<label for="access_private"> <input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private" />
<div class="boxselector_header"> <label for="access_private">
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i> <div class="boxselector_header">
Private <pr-icon icon="'eye-off'" feather="true"></pr-icon>
</div> Private
<p> I want to this resource to be manageable by myself only </p> </div>
</label> <p> I want to this resource to be manageable by myself only </p>
</div> </label>
<div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0"> </div>
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" /> <div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0">
<label for="access_restricted"> <input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<div class="boxselector_header"> <label for="access_restricted">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i> <div class="boxselector_header">
Restricted <pr-icon icon="'users'" feather="true"></pr-icon>
</div>
<p ng-if="$ctrl.availableTeams.length === 1"> Restricted
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b </div>
>) to be able to manage this resource <p ng-if="$ctrl.availableTeams.length === 1">
</p> I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b
<p ng-if="$ctrl.availableTeams.length > 1"> I want to restrict the management of this resource to one or more of my teams </p> >) to be able to manage this resource
</label> </p>
<p ng-if="$ctrl.availableTeams.length > 1"> I want to restrict the management of this resource to one or more of my teams </p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -12,7 +12,7 @@
<option value="" label="Select a Custom template" disabled selected="selected"> </option> <option value="" label="Select a Custom template" disabled selected="selected"> </option>
</select> </select>
<span class="small text-muted pt-[7px]" ng-if="!$ctrl.templates.length"> <span class="small text-muted pt-[7px]" ng-if="!$ctrl.templates.length">
No custom templates are available. Head over to the <a class="hyperlink" ui-state="$ctrl.newTemplatePath">custom template view</a> to create one. No custom templates are available. Head over to the <a ui-state="$ctrl.newTemplatePath">custom template view</a> to create one.
</span> </span>
</div> </div>
</div> </div>
@@ -40,19 +40,6 @@
@apply text-blue-7; @apply text-blue-7;
} }
.datatable tr > td a:not(.btn) {
color: var(--ui-blue-8);
}
.datatable tr > td a:not(.btn):hover,
.datatable tr > td a:not(.btn):focus {
text-decoration: underline;
}
.datatable tr > td a.actions {
color: currentColor;
}
.toolBar .actionBar { .toolBar .actionBar {
display: inline-flex; display: inline-flex;
} }
@@ -77,10 +77,12 @@
<input id="select_{{ $index }}" type="checkbox" disabled /> <input id="select_{{ $index }}" type="checkbox" disabled />
<label for="select_{{ $index }}"></label> <label for="select_{{ $index }}"></label>
</span> </span>
<span>DockerHub (anonymous)</span> <span><default-registry-name></default-registry-name></span>
</td>
<td> <default-registry-domain></default-registry-domain> </td>
<td>
<default-registry-action ng-if="$ctrl.isAdmin && !$ctrl.endpointType"></default-registry-action>
</td> </td>
<td> docker.io </td>
<td> - </td>
</tr> </tr>
<tr <tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
@@ -24,47 +24,49 @@
</div> </div>
<div class="form-group"></div> <div class="form-group"></div>
<!-- endpoint-tls-mode --> <!-- endpoint-tls-mode -->
<div class="form-group" style="margin-bottom: 0" ng-if="$ctrl.formData.TLS"> <div class="form-group" ng-if="$ctrl.formData.TLS">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca" /> <div>
<label for="tls_client_ca"> <input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca" />
<div class="boxselector_header"> <label for="tls_client_ca">
<pr-icon icon="'shield'" feather="true"></pr-icon> <div class="boxselector_header">
TLS with server and client verification <pr-icon icon="'shield'" feather="true"></pr-icon>
</div> TLS with server and client verification
<p>Use client certificates and server verification</p> </div>
</label> <p>Use client certificates and server verification</p>
</div> </label>
<div> </div>
<input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca" /> <div>
<label for="tls_client_noca"> <input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca" />
<div class="boxselector_header"> <label for="tls_client_noca">
<pr-icon icon="'shield'" feather="true"></pr-icon> <div class="boxselector_header">
TLS with client verification only <pr-icon icon="'shield'" feather="true"></pr-icon>
</div> TLS with client verification only
<p>Use client certificates without server verification</p> </div>
</label> <p>Use client certificates without server verification</p>
</div> </label>
<div> </div>
<input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca" /> <div>
<label for="tls_ca"> <input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca" />
<div class="boxselector_header"> <label for="tls_ca">
<pr-icon icon="'shield'" feather="true"></pr-icon> <div class="boxselector_header">
TLS with server verification only <pr-icon icon="'shield'" feather="true"></pr-icon>
</div> TLS with server verification only
<p>Only verify the server certificate</p> </div>
</label> <p>Only verify the server certificate</p>
</div> </label>
<div> </div>
<input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only" /> <div>
<label for="tls_only"> <input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only" />
<div class="boxselector_header"> <label for="tls_only">
<pr-icon icon="'shield'" feather="true"></pr-icon> <div class="boxselector_header">
TLS only <pr-icon icon="'shield'" feather="true"></pr-icon>
</div> TLS only
<p>No server/client verification</p> </div>
</label> <p>No server/client verification</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,7 +1,8 @@
class WebEditorFormController { class WebEditorFormController {
/* @ngInject */ /* @ngInject */
constructor() { constructor(BROWSER_OS_PLATFORM) {
this.editorUpdate = this.editorUpdate.bind(this); this.editorUpdate = this.editorUpdate.bind(this);
this.BROWSER_OS_PLATFORM = BROWSER_OS_PLATFORM;
} }
editorUpdate(cm) { editorUpdate(cm) {
@@ -1,7 +1,41 @@
<ng-form name="$ctrl.webEditorForm"> <ng-form name="$ctrl.webEditorForm">
<div class="web-editor overflow-auto"> <div class="web-editor overflow-auto">
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title">Web editor</div> <div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title pr-0"
<div class="trancluded-item form-group col-sm-12 col-lg-12 text-muted small" ng-transclude="description"></div> >Web editor
<div class="text-muted small vertical-center float-right mt-0">
<span ng-if="$ctrl.BROWSER_OS_PLATFORM !== 'mac'" class="vertical-center">Ctrl+F for search</span>
<span ng-if="$ctrl.BROWSER_OS_PLATFORM === 'mac'" class="vertical-center">Cmd+F for search</span>
<portainer-tooltip
ng-if="$ctrl.BROWSER_OS_PLATFORM !== 'mac'"
message="'Ctrl+F - Start searching <br />
Ctrl+G - Find next <br />
Ctrl+Shift+G - Find previous <br />
Ctrl+Shift+F - Replace <br />
Ctrl+Shift+R - Replace all <br />
Alt+G - Jump to line <br />
Alt+F - Persistent search: <br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Enter - Find next <br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Shift+Enter - Find previous <br />'"
class-name="'[&>span]:!text-left'"
>
</portainer-tooltip>
<portainer-tooltip
ng-if="$ctrl.BROWSER_OS_PLATFORM === 'mac'"
message="'Cmd+F - Start searching <br />
Cmd+G - Find next <br />
Cmd+Shift+G - Find previous <br />
Cmd+Option+F - Replace <br />
Cmd+Option+R - Replace all <br />
Option+G - Jump to line <br />
Option+F - Persistent search: <br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Enter - Find next <br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Shift+Enter - Find previous <br />'"
class-name="'[&>span]:!text-left'"
>
</portainer-tooltip>
</div>
</div>
<div class="trancluded-item form-group col-sm-9 col-lg-10 text-muted small" ng-transclude="description"></div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12 col-lg-12"> <div class="col-sm-12 col-lg-12">
<code-editor <code-editor
@@ -13,7 +13,9 @@
</div> </div>
<div class="small vertical-center" ng-if="$ctrl.model.RepositoryAutomaticUpdates"> <div class="small vertical-center" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> <pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
<span class="text-muted">Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.</span> <span class="text-muted"
>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption. Do you wish to continue?</span
>
</div> </div>
<div class="form-group mt-2" ng-if="$ctrl.model.RepositoryAutomaticUpdates"> <div class="form-group mt-2" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<label for="repository_mechanism" class="col-lg-2 col-sm-3 control-label text-left"> Mechanism </label> <label for="repository_mechanism" class="col-lg-2 col-sm-3 control-label text-left"> Mechanism </label>
@@ -48,14 +48,24 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<a class="small interactive vertical-center" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;"> <button
type="button"
class="btn btn-link btn-sm hover:no-underline !ml-0 p-0"
ng-if="!$ctrl.state.overrideConfiguration"
ng-click="$ctrl.state.overrideConfiguration = true;"
>
<pr-icon icon="'tool'" feather="true"></pr-icon> <pr-icon icon="'tool'" feather="true"></pr-icon>
Override default configuration Override default configuration
</a> </button>
<a class="small interactive vertical-center" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.resetDefaults()"> <button
type="button"
class="btn btn-link btn-sm hover:no-underline !ml-0 p-0"
ng-if="$ctrl.state.overrideConfiguration"
ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.resetDefaults()"
>
<pr-icon icon="'settings'" feather="true"></pr-icon> <pr-icon icon="'settings'" feather="true"></pr-icon>
Use default configuration Use default configuration
</a> </button>
</div> </div>
</div> </div>
@@ -103,12 +103,12 @@ class StackRedeployGitFormController {
async submit() { async submit() {
const tplCrop = const tplCrop =
'<div>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.</div>' + '<div>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption. Do you wish to continue?</div>' +
'<div"><div style="position: absolute; right: 110px; top: 68px; z-index: 999">' + '<div"><div style="position: absolute; right: 5px; top: 84px; z-index: 999">' +
'<be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div></div>'; '<be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div></div>';
const template = angular.element(tplCrop); const template = angular.element(tplCrop);
const html = this.$compile(template)(this.$scope); const html = this.$compile(template)(this.$scope);
this.ModalService.confirmStackUpdate(html, true, true, 'btn-warning', async (result) => { this.ModalService.confirmStackUpdate(html, true, false, 'btn-warning', async (result) => {
if (!result) { if (!result) {
return; return;
} }
@@ -2,10 +2,8 @@
<rd-widget> <rd-widget>
<rd-widget-header icon="sliders" feather-icon="true" title-text="User theme"></rd-widget-header> <rd-widget-header icon="sliders" feather-icon="true" title-text="User theme"></rd-widget-header>
<rd-widget-body> <rd-widget-body>
<form class="theme-panel"> <form class="form-horizontal">
<!-- Theme Selector-->
<box-selector radio-name="'theme'" value="$ctrl.state.userTheme" options="$ctrl.state.availableThemes" on-change="($ctrl.setTheme)"></box-selector> <box-selector radio-name="'theme'" value="$ctrl.state.userTheme" options="$ctrl.state.availableThemes" on-change="($ctrl.setTheme)"></box-selector>
<!-- !Theme -->
</form> </form>
<p class="mt-2 vertical-center"> <p class="mt-2 vertical-center">
<pr-icon icon="'alert-circle'" class-name="'icon-primary'" feather="true"></pr-icon> <pr-icon icon="'alert-circle'" class-name="'icon-primary'" feather="true"></pr-icon>
@@ -105,11 +105,11 @@ export interface EnvironmentOptions {
url?: string; url?: string;
publicUrl?: string; publicUrl?: string;
meta?: EnvironmentMetadata; meta?: EnvironmentMetadata;
checkinInterval?: number;
azure?: AzureSettings; azure?: AzureSettings;
tls?: TLSSettings; tls?: TLSSettings;
isEdgeDevice?: boolean; isEdgeDevice?: boolean;
gpus?: Gpu[]; gpus?: Gpu[];
pollFrequency?: number;
} }
interface CreateRemoteEnvironment { interface CreateRemoteEnvironment {
@@ -130,7 +130,7 @@ export async function createRemoteEnvironment({
}: CreateRemoteEnvironment) { }: CreateRemoteEnvironment) {
return createEnvironment(name, creationType, { return createEnvironment(name, creationType, {
...options, ...options,
url: `${url}`, url: `tcp://${url}`,
}); });
} }
@@ -175,6 +175,7 @@ export function createEdgeAgentEnvironment({
meta = { tagIds: [] }, meta = { tagIds: [] },
gpus = [], gpus = [],
isEdgeDevice, isEdgeDevice,
pollFrequency,
}: CreateEdgeAgentEnvironment) { }: CreateEdgeAgentEnvironment) {
return createEnvironment( return createEnvironment(
name, name,
@@ -187,6 +188,7 @@ export function createEdgeAgentEnvironment({
}, },
gpus, gpus,
isEdgeDevice, isEdgeDevice,
pollFrequency,
...meta, ...meta,
} }
); );
@@ -211,7 +213,7 @@ async function createEnvironment(
PublicURL: options.publicUrl, PublicURL: options.publicUrl,
GroupID: groupId, GroupID: groupId,
TagIds: arrayToJson(tagIds), TagIds: arrayToJson(tagIds),
CheckinInterval: options.checkinInterval, CheckinInterval: options.pollFrequency,
IsEdgeDevice: options.isEdgeDevice, IsEdgeDevice: options.isEdgeDevice,
Gpus: arrayToJson(options.gpus), Gpus: arrayToJson(options.gpus),
}; };
+1
View File
@@ -30,4 +30,5 @@ export enum FeatureId {
STACK_WEBHOOK = 'stack-webhook', STACK_WEBHOOK = 'stack-webhook',
CONTAINER_WEBHOOK = 'container-webhook', CONTAINER_WEBHOOK = 'container-webhook',
POD_SECURITY_POLICY_CONSTRAINT = 'pod-security-policy-constraint', POD_SECURITY_POLICY_CONSTRAINT = 'pod-security-policy-constraint',
HIDE_DOCKER_HUB_ANONYMOUS = 'hide-docker-hub-anonymous',
} }
@@ -35,6 +35,7 @@ export async function init(edition: Edition) {
[FeatureId.STACK_WEBHOOK]: Edition.BE, [FeatureId.STACK_WEBHOOK]: Edition.BE,
[FeatureId.CONTAINER_WEBHOOK]: Edition.BE, [FeatureId.CONTAINER_WEBHOOK]: Edition.BE,
[FeatureId.POD_SECURITY_POLICY_CONSTRAINT]: Edition.BE, [FeatureId.POD_SECURITY_POLICY_CONSTRAINT]: Edition.BE,
[FeatureId.HIDE_DOCKER_HUB_ANONYMOUS]: Edition.BE,
}; };
state.currentEdition = currentEdition; state.currentEdition = currentEdition;
+5 -4
View File
@@ -2,6 +2,7 @@ import moment from 'moment';
import _ from 'lodash-es'; import _ from 'lodash-es';
import filesize from 'filesize'; import filesize from 'filesize';
import { Eye, EyeOff, Users } from 'react-feather';
import { ResourceControlOwnership as RCO } from '@/portainer/access-control/types'; import { ResourceControlOwnership as RCO } from '@/portainer/access-control/types';
export function truncateLeftRight(text, max, left, right) { export function truncateLeftRight(text, max, left, right) {
@@ -106,13 +107,13 @@ export function environmentTypeIcon(type) {
export function ownershipIcon(ownership) { export function ownershipIcon(ownership) {
switch (ownership) { switch (ownership) {
case RCO.PRIVATE: case RCO.PRIVATE:
return 'eye-off'; return EyeOff;
case RCO.ADMINISTRATORS: case RCO.ADMINISTRATORS:
return 'eye-off'; return EyeOff;
case RCO.RESTRICTED: case RCO.RESTRICTED:
return 'users'; return Users;
default: default:
return 'eye'; return Eye;
} }
} }
@@ -46,12 +46,11 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
<div className={styles.root}> <div className={styles.root}>
<button <button
type="button" type="button"
color="link"
onClick={() => onClick(environment)} onClick={() => onClick(environment)}
className={styles.wrapperButton} className={styles.wrapperButton}
> >
<Link <Link
className={clsx('blocklist-item', styles.item)} className={clsx('blocklist-item no-link', styles.item)}
to={route} to={route}
params={{ params={{
endpointId: environment.Id, endpointId: environment.Id,
@@ -9,7 +9,6 @@
.refresh-environments-button { .refresh-environments-button {
margin-left: 0 !important; margin-left: 0 !important;
padding: 8px 15px;
} }
.filter-container { .filter-container {
@@ -73,7 +72,6 @@
.filterSearchbar { .filterSearchbar {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
padding-left: 10px;
} }
.filterSearchbar input[type='text'] { .filterSearchbar input[type='text'] {
@@ -130,7 +130,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
status: statusFilter, status: statusFilter,
tagIds: tagFilter?.length ? tagFilter : undefined, tagIds: tagFilter?.length ? tagFilter : undefined,
groupIds: groupFilter, groupIds: groupFilter,
edgeDevice: getEdgeDeviceFilter(connectionTypes.map((p) => p.value)), edgeDevice: false,
tagsPartialMatch: true, tagsPartialMatch: true,
agentVersions: agentVersions.map((a) => a.value), agentVersions: agentVersions.map((a) => a.value),
}; };
@@ -192,8 +192,10 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
<Button <Button
onClick={onRefresh} onClick={onRefresh}
data-cy="home-refreshEndpointsButton" data-cy="home-refreshEndpointsButton"
size="medium"
color="secondary"
className={clsx( className={clsx(
'vertical-center', 'vertical-center !ml-0',
styles.refreshEnvironmentsButton styles.refreshEnvironmentsButton
)} )}
> >
@@ -215,7 +217,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
}} }}
/> />
</div> </div>
<div className={styles.filterSearchbar}> <div className={clsx(styles.filterSearchbar, 'ml-3')}>
<FilterSearchBar <FilterSearchBar
value={searchBarValue} value={searchBarValue}
onChange={setSearchBarValue} onChange={setSearchBarValue}
@@ -331,19 +333,6 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
</> </>
); );
function getEdgeDeviceFilter(connectionTypes: ConnectionType[]) {
// show both types of edge agent if both are selected or if no connection type is selected
if (
connectionTypes.length === 0 ||
(connectionTypes.includes(ConnectionType.EdgeAgent) &&
connectionTypes.includes(ConnectionType.EdgeDevice))
) {
return undefined;
}
return connectionTypes.includes(ConnectionType.EdgeDevice);
}
function getTypes( function getTypes(
platformTypes: PlatformType[], platformTypes: PlatformType[],
connectionTypes: ConnectionType[] connectionTypes: ConnectionType[]
@@ -495,7 +484,6 @@ function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
{ value: ConnectionType.API, label: 'API' }, { value: ConnectionType.API, label: 'API' },
{ value: ConnectionType.Agent, label: 'Agent' }, { value: ConnectionType.Agent, label: 'Agent' },
{ value: ConnectionType.EdgeAgent, label: 'Edge Agent' }, { value: ConnectionType.EdgeAgent, label: 'Edge Agent' },
{ value: ConnectionType.EdgeDevice, label: 'Edge Device' },
]; ];
if (platformTypes.length === 0) { if (platformTypes.length === 0) {
@@ -1,3 +0,0 @@
.kubeconfig-button {
padding: 8px 15px;
}
@@ -8,7 +8,6 @@ import { Query } from '@/portainer/environments/queries/useEnvironmentList';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import styles from './KubeconfigButton.module.css';
import { KubeconfigPrompt } from './KubeconfigPrompt'; import { KubeconfigPrompt } from './KubeconfigPrompt';
import '@reach/dialog/styles.css'; import '@reach/dialog/styles.css';
@@ -30,8 +29,8 @@ export function KubeconfigButton({ environments, envQueryParams }: Props) {
return ( return (
<> <>
<Button className={styles.kubeconfigButton} onClick={handleClick}> <Button onClick={handleClick} size="medium" className="!ml-3">
<Download className="feather-icon-white" aria-hidden="true" />{' '} <Download className="feather icon-white" aria-hidden="true" />{' '}
Kubeconfig Kubeconfig
</Button> </Button>
{prompt()} {prompt()}
@@ -1,3 +1,5 @@
import { X } from 'react-feather';
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { DialogOverlay } from '@reach/dialog'; import { DialogOverlay } from '@reach/dialog';
@@ -59,7 +61,7 @@ export function KubeconfigPrompt({
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<button type="button" className="close" onClick={onClose}> <button type="button" className="close" onClick={onClose}>
× <X />
</button> </button>
<h5 className="modal-title">Download kubeconfig file</h5> <h5 className="modal-title">Download kubeconfig file</h5>
</div> </div>
@@ -74,16 +76,24 @@ export function KubeconfigPrompt({
</div> </div>
</form> </form>
<br /> <br />
<Checkbox <div className="h-8 flex items-center">
id="settings-container-truncate-nae" <Checkbox
label="Select all (in this page)" id="settings-container-truncate-name"
checked={isAllPageSelected} label="Select all (in this page)"
onChange={handleSelectAll} checked={isAllPageSelected}
/> onChange={handleSelectAll}
/>
</div>
<div className="datatable"> <div className="datatable">
<div className="bootbox-checkbox-list"> <div className="bootbox-checkbox-list">
{environments.map((env) => ( {environments.map((env) => (
<div className={styles.checkbox}> <div
key={env.Id}
className={clsx(
styles.checkbox,
'h-8 flex items-center pt-1'
)}
>
<Checkbox <Checkbox
id={`${env.Id}`} id={`${env.Id}`}
label={`${env.Name} (${env.URL})`} label={`${env.Name} (${env.URL})`}
@@ -95,7 +105,7 @@ export function KubeconfigPrompt({
</div> </div>
))} ))}
</div> </div>
<div className="footer"> <div className="pt-3 flex justify-end w-full">
<PaginationControls <PaginationControls
showAll={totalCount <= 100} showAll={totalCount <= 100}
page={page} page={page}
@@ -111,7 +121,9 @@ export function KubeconfigPrompt({
<Button onClick={onClose} color="default"> <Button onClick={onClose} color="default">
Cancel Cancel
</Button> </Button>
<Button onClick={handleDownload}>Download File</Button> <Button onClick={handleDownload} disabled={selectionSize < 1}>
Download File
</Button>
</div> </div>
</div> </div>
</div> </div>
@@ -143,7 +155,7 @@ export function KubeconfigPrompt({
} }
export function expiryMessage(expiry: string) { export function expiryMessage(expiry: string) {
const prefix = 'Kubeconfig file will'; const prefix = 'The kubeconfig file will';
switch (expiry) { switch (expiry) {
case '24h': case '24h':
return `${prefix} expire in 1 day.`; return `${prefix} expire in 1 day.`;
+1
View File
@@ -5,6 +5,7 @@ import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
interface UIState { interface UIState {
dismissedInfoPanels: Record<string, string>; dismissedInfoPanels: Record<string, string>;
dismissedInfoHash: string; dismissedInfoHash: string;
dismissedUpdateVersion: string;
} }
type UIStateService = [UIState, (state: UIState) => void]; type UIStateService = [UIState, (state: UIState) => void];
+1
View File
@@ -36,6 +36,7 @@ export function PublicSettingsViewModel(settings) {
this.KubeconfigExpiry = settings.KubeconfigExpiry; this.KubeconfigExpiry = settings.KubeconfigExpiry;
this.Features = settings.Features; this.Features = settings.Features;
this.Edge = new EdgeSettingsViewModel(settings.Edge); this.Edge = new EdgeSettingsViewModel(settings.Edge);
this.DefaultRegistry = settings.DefaultRegistry;
} }
export function InternalAuthSettingsViewModel(data) { export function InternalAuthSettingsViewModel(data) {
@@ -1,8 +1,11 @@
import { Edit } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums'; import { FeatureId } from '@/portainer/feature-flags/enums';
import Microsoft from '@/assets/ico/vendor/microsoft.svg?c'; import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
import Google from '@/assets/ico/vendor/google.svg?c'; import Google from '@/assets/ico/vendor/google.svg?c';
import Github from '@/assets/ico/vendor/github.svg?c'; import Github from '@/assets/ico/vendor/github.svg?c';
import Custom from '@/assets/ico/custom.svg?c';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
export const options = [ export const options = [
{ {
@@ -32,7 +35,7 @@ export const options = [
}, },
{ {
id: 'custom', id: 'custom',
icon: Custom, icon: <BadgeIcon icon={Edit} />,
label: 'Custom', label: 'Custom',
description: 'Custom OAuth provider', description: 'Custom OAuth provider',
value: 'custom', value: 'custom',
+14 -2
View File
@@ -15,6 +15,7 @@ import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell';
import { DashboardItem } from '@@/DashboardItem'; import { DashboardItem } from '@@/DashboardItem';
import { SearchBar } from '@@/datatables/SearchBar'; import { SearchBar } from '@@/datatables/SearchBar';
import { FallbackImage } from '@@/FallbackImage'; import { FallbackImage } from '@@/FallbackImage';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
import { fileUploadField } from './file-upload-field'; import { fileUploadField } from './file-upload-field';
import { switchField } from './switch-field'; import { switchField } from './switch-field';
@@ -28,7 +29,7 @@ export const componentsModule = angular
) )
.component( .component(
'portainerTooltip', 'portainerTooltip',
react2angular(Tooltip, ['message', 'position']) react2angular(Tooltip, ['message', 'position', 'className'])
) )
.component('fileUploadField', fileUploadField) .component('fileUploadField', fileUploadField)
.component('porSwitchField', switchField) .component('porSwitchField', switchField)
@@ -49,7 +50,14 @@ export const componentsModule = angular
.component('viewLoading', r2a(ViewLoading, ['message'])) .component('viewLoading', r2a(ViewLoading, ['message']))
.component( .component(
'pageHeader', 'pageHeader',
r2a(PageHeader, ['title', 'breadcrumbs', 'loading', 'onReload', 'reload']) r2a(PageHeader, [
'id',
'title',
'breadcrumbs',
'loading',
'onReload',
'reload',
])
) )
.component( .component(
'fallbackImage', 'fallbackImage',
@@ -76,4 +84,8 @@ export const componentsModule = angular
.component( .component(
'datatableSearchbar', 'datatableSearchbar',
r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder']) r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder'])
)
.component(
'boxSelectorBadgeIcon',
react2angular(BadgeIcon, ['featherIcon', 'icon'])
).name; ).name;
+8
View File
@@ -2,11 +2,19 @@ import angular from 'angular';
import { r2a } from '@/react-tools/react2angular'; import { r2a } from '@/react-tools/react2angular';
import { CreateAccessToken } from '@/react/portainer/account/CreateAccessTokenView'; import { CreateAccessToken } from '@/react/portainer/account/CreateAccessTokenView';
import {
DefaultRegistryAction,
DefaultRegistryDomain,
DefaultRegistryName,
} from '@/react/portainer/registries/ListView/DefaultRegistry';
import { wizardModule } from './wizard'; import { wizardModule } from './wizard';
export const viewsModule = angular export const viewsModule = angular
.module('portainer.app.react.views', [wizardModule]) .module('portainer.app.react.views', [wizardModule])
.component('defaultRegistryName', r2a(DefaultRegistryName, []))
.component('defaultRegistryAction', r2a(DefaultRegistryAction, []))
.component('defaultRegistryDomain', r2a(DefaultRegistryDomain, []))
.component( .component(
'createAccessToken', 'createAccessToken',
r2a(CreateAccessToken, ['onSubmit', 'onError']) r2a(CreateAccessToken, ['onSubmit', 'onError'])
+1 -1
View File
@@ -8,7 +8,7 @@ import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/searchcursor.js'; import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/jump-to-line.js'; import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js'; import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/dialog/dialog.css'; import './codeMirrorDialog.css';
angular.module('portainer.app').factory('CodeMirrorService', function CodeMirrorService() { angular.module('portainer.app').factory('CodeMirrorService', function CodeMirrorService() {
'use strict'; 'use strict';
@@ -0,0 +1,49 @@
/* styles from https://github.com/codemirror/codemirror5/blob/master/addon/dialog/dialog.css with the button styles updated */
.CodeMirror-dialog {
position: absolute;
left: 0;
right: 0;
background: inherit;
z-index: 15;
padding: 0.1em 0.8em;
overflow: hidden;
color: inherit;
}
.CodeMirror-dialog-top {
border-bottom: 1px solid #eee;
top: 0;
}
.CodeMirror-dialog-bottom {
border-top: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog input {
border: none;
outline: none;
background: transparent;
width: 20em;
color: inherit;
font-family: monospace;
}
.CodeMirror-dialog button {
/* apply styles from btn-default */
@apply bg-white border-gray-5 text-gray-9;
@apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10;
/* 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;
/* highcontrast mode */
@apply th-highcontrast:bg-gray-warm-10 th-highcontrast:border-gray-warm-7 th-highcontrast:text-white;
@apply th-highcontrast:hover:bg-gray-warm-9 th-highcontrast:hover:border-gray-6 th-highcontrast:hover:text-white;
@apply font-sans;
@apply border-solid border;
font-size: 85%;
padding: 0px 8px;
border-radius: 8px;
}
@@ -167,8 +167,8 @@ export function confirmStackUpdate(
confirmButtonClassName: string | undefined, confirmButtonClassName: string | undefined,
callback: PromptCallback callback: PromptCallback
) { ) {
const sanitizedMessage = sanitize(message); const sanitizedMessage =
typeof message === 'string' ? sanitize(message) : message;
const box = prompt({ const box = prompt({
title: buildTitle('Are you sure?'), title: buildTitle('Are you sure?'),
inputType: 'checkbox', inputType: 'checkbox',
@@ -197,7 +197,8 @@ export function confirmStackUpdate(
'position: relative; display: block; margin-top: 10px; margin-bottom: 10px;' 'position: relative; display: block; margin-top: 10px; margin-bottom: 10px;'
); );
const checkboxLabel = box.find('.form-check-label'); const checkboxLabel = box.find('.form-check-label');
checkboxLabel.addClass('switch box-selector-item limited business'); checkboxLabel.addClass('switch box-selector-item limited business mt-4');
checkboxLabel.prop('style', 'width: 100%');
const switchEle = checkboxLabel.find('i'); const switchEle = checkboxLabel.find('i');
switchEle.prop('style', 'margin-left:20px'); switchEle.prop('style', 'margin-left:20px');
} }
+1
View File
@@ -26,6 +26,7 @@ function StateManagerFactory(
dismissedInfoPanels: {}, dismissedInfoPanels: {},
dismissedInfoHash: '', dismissedInfoHash: '',
timesPasswordChangeSkipped: {}, timesPasswordChangeSkipped: {},
dismissedUpdateVersion: '',
}, },
}; };
@@ -109,7 +109,7 @@
</label> </label>
<label class="switch ml-7 my-0" ng-class="{ 'business limited': $ctrl.isLimitedFeatureSelfContained }"> <label class="switch ml-7 my-0" ng-class="{ 'business limited': $ctrl.isLimitedFeatureSelfContained }">
<input id="admin-auto-populate" ng-disabled="!$ctrl.enableAssignAdminGroup" name="admin-auto-populate" type="checkbox" ng-model="$ctrl.settings.AdminAutoPopulate" /> <input id="admin-auto-populate" ng-disabled="!$ctrl.enableAssignAdminGroup" name="admin-auto-populate" type="checkbox" ng-model="$ctrl.settings.AdminAutoPopulate" />
<span class="slider round"></span> <span class="slider round before:content-['']"></span>
</label> </label>
</div> </div>
</div> </div>
@@ -1,6 +1,9 @@
import { Edit } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums'; import { FeatureId } from '@/portainer/feature-flags/enums';
import Openldap from '@/assets/ico/vendor/openldap.svg?c'; import Openldap from '@/assets/ico/vendor/openldap.svg?c';
import Custom from '@/assets/ico/custom.svg?c';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
const SERVER_TYPES = { const SERVER_TYPES = {
CUSTOM: 0, CUSTOM: 0,
@@ -11,7 +14,7 @@ const SERVER_TYPES = {
export const options = [ export const options = [
{ {
id: 'ldap_custom', id: 'ldap_custom',
icon: Custom, icon: <BadgeIcon icon={Edit} />,
label: 'Custom', label: 'Custom',
value: SERVER_TYPES.CUSTOM, value: SERVER_TYPES.CUSTOM,
}, },
+14 -1
View File
@@ -12,8 +12,9 @@ import {
getSettings, getSettings,
updateSettings, updateSettings,
getPublicSettings, getPublicSettings,
updateDefaultRegistry,
} from './settings.service'; } from './settings.service';
import { Settings } from './types'; import { DefaultRegistry, Settings } from './types';
export function usePublicSettings<T = PublicSettingsViewModel>({ export function usePublicSettings<T = PublicSettingsViewModel>({
enabled, enabled,
@@ -51,3 +52,15 @@ export function useUpdateSettingsMutation() {
) )
); );
} }
export function useUpdateDefaultRegistrySettingsMutation() {
const queryClient = useQueryClient();
return useMutation(
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
mutationOptions(
withInvalidate(queryClient, [['settings']]),
withError('Unable to update default registry settings')
)
);
}
+14 -1
View File
@@ -2,7 +2,7 @@ import { PublicSettingsViewModel } from '@/portainer/models/settings';
import axios, { parseAxiosError } from '../services/axios'; import axios, { parseAxiosError } from '../services/axios';
import { PublicSettingsResponse, Settings } from './types'; import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
export async function getPublicSettings() { export async function getPublicSettings() {
try { try {
@@ -38,6 +38,19 @@ export async function updateSettings(settings: Partial<Settings>) {
} }
} }
export async function updateDefaultRegistry(
defaultRegistry: Partial<DefaultRegistry>
) {
try {
await axios.put(buildUrl('default_registry'), defaultRegistry);
} catch (e) {
throw parseAxiosError(
e as Error,
'Unable to update default registry settings'
);
}
}
function buildUrl(subResource?: string, action?: string) { function buildUrl(subResource?: string, action?: string) {
let url = 'settings'; let url = 'settings';
if (subResource) { if (subResource) {
+4
View File
@@ -89,6 +89,10 @@ enum AuthenticationMethod {
type Feature = string; type Feature = string;
export interface DefaultRegistry {
Hide: boolean;
}
export interface Settings { export interface Settings {
LogoURL: string; LogoURL: string;
BlackListedLabels: Pair[]; BlackListedLabels: Pair[];
@@ -8,7 +8,7 @@
</div> </div>
Activity Logs Activity Logs
<be-feature-indicator feature="{{::$ctrl.feature}}"></be-feature-indicator> <be-feature-indicator feature="$ctrl.feature"></be-feature-indicator>
</div> </div>
<div class="vertical-center"> <div class="vertical-center">
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar> <datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar>
@@ -8,7 +8,7 @@
</div> </div>
Authentication Events Authentication Events
<be-feature-indicator feature="{{::$ctrl.feature}}"></be-feature-indicator> <be-feature-indicator feature="$ctrl.feature"></be-feature-indicator>
</div> </div>
<div class="vertical-center"> <div class="vertical-center">
<datatable-searchbar on-change="($ctrl.onChangeKeyword)"></datatable-searchbar> <datatable-searchbar on-change="($ctrl.onChangeKeyword)"></datatable-searchbar>
@@ -15,38 +15,39 @@
<!-- build-method --> <!-- build-method -->
<div ng-if="!$ctrl.state.fromStack"> <div ng-if="!$ctrl.state.fromStack">
<div class="col-sm-12 form-section-title"> Build method </div> <div class="col-sm-12 form-section-title"> Build method </div>
<div class="form-group"></div> <div class="form-group">
<div class="form-group mb-0"> <div class="col-sm-12">
<div class="boxselector_wrapper"> <div class="boxselector_wrapper">
<div> <div>
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" /> <input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
<label for="method_editor"> <label for="method_editor">
<div class="boxselector_header"> <div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon> <pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor Web editor
</div> </div>
<p>Use our Web editor</p> <p>Use our Web editor</p>
</label> </label>
</div> </div>
<div> <div>
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" /> <input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
<label for="method_upload"> <label for="method_upload">
<div class="boxselector_header"> <div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon> <pr-icon icon="'upload'" feather="true"></pr-icon>
Upload Upload
</div> </div>
<p>Upload from your computer</p> <p>Upload from your computer</p>
</label> </label>
</div> </div>
<div> <div>
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" /> <input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
<label for="method_repository"> <label for="method_repository">
<div class="boxselector_header"> <div class="boxselector_header">
<pr-icon icon="'git-pull-request'" feather="true"></pr-icon> <pr-icon icon="'git-pull-request'" feather="true"></pr-icon>
Repository Repository
</div> </div>
<p>Use a git repository</p> <p>Use a git repository</p>
</label> </label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -16,18 +16,19 @@
<!-- !name-input --> <!-- !name-input -->
<!-- build-method --> <!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div> <div class="col-sm-12 form-section-title"> Profile configuration </div>
<div class="form-group"></div> <div class="form-group">
<div class="form-group" style="margin-bottom: 0"> <div class="col-sm-12">
<div class="boxselector_wrapper"> <div class="boxselector_wrapper">
<div> <div>
<input type="radio" id="method_editor" ng-model="state.method" value="editor" /> <input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<label for="method_editor"> <label for="method_editor">
<div class="boxselector_header"> <div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon> <pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor Web editor
</div> </div>
<p>Use our Web editor</p> <p>Use our Web editor</p>
</label> </label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -17,17 +17,19 @@
<!-- build-method --> <!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div> <div class="col-sm-12 form-section-title"> Profile configuration </div>
<div class="form-group"></div> <div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div> <div class="boxselector_wrapper">
<input type="radio" id="method_editor" ng-model="state.method" value="editor" /> <div>
<label for="method_editor"> <input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<div class="boxselector_header"> <label for="method_editor">
<pr-icon icon="'edit'" feather="true"></pr-icon> <div class="boxselector_header">
Web editor <pr-icon icon="'edit'" feather="true"></pr-icon>
</div> Web editor
<p>Use our Web editor</p> </div>
</label> <p>Use our Web editor</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -110,9 +110,11 @@
<span ng-if="!state.agentEndpoint">Environment URL</span> <span ng-if="!state.agentEndpoint">Environment URL</span>
<span ng-if="state.agentEndpoint">Environment address</span> <span ng-if="state.agentEndpoint">Environment address</span>
<portainer-tooltip <portainer-tooltip
ng-if="!state.agentEndpoint"
message="'URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it.'" message="'URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it.'"
> >
</portainer-tooltip> </portainer-tooltip>
<portainer-tooltip ng-if="state.agentEndpoint" message="'The address for the Portainer agent in the format <HOST>:<PORT> or <IP>:<PORT>'"> </portainer-tooltip>
</label> </label>
<div class="col-sm-9 col-lg-10"> <div class="col-sm-9 col-lg-10">
<input <input
+25 -21
View File
@@ -138,29 +138,33 @@
</div> </div>
</div> </div>
<!-- !note --> <!-- !note -->
<div class="boxselector_wrapper"> <div class="form-group">
<div> <div class="col-sm-12">
<input type="radio" id="restore_file" checked="checked" /> <div class="boxselector_wrapper">
<label for="restore_file" data-cy="init-selectLocalFile"> <div>
<div class="boxselector_header"> <input type="radio" id="restore_file" checked="checked" />
<pr-icon icon="'upload'" feather="true"></pr-icon> <label for="restore_file" data-cy="init-selectLocalFile">
Upload backup file <div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload backup file
</div>
<p></p>
</label>
</div> </div>
<p></p> <div>
</label> <input type="radio" id="restore_s3" disabled />
</div> <label for="restore_s3" class="boxselector_disabled">
<div> <div class="boxselector_header">
<input type="radio" id="restore_s3" disabled /> <pr-icon icon="'download'" feather="true"></pr-icon>
<label for="restore_s3" class="boxselector_disabled"> Retrieve from S3
<div class="boxselector_header"> </div>
<pr-icon icon="'download'" feather="true"></pr-icon> <p
Retrieve from S3 >This feature is available in
<a class="hyperlink" href="https://www.portainer.io/business-upsell?from=restore-s3-form" target="_blank"> Portainer Business Edition</a></p
>
</label>
</div> </div>
<p </div>
>This feature is available in
<a class="hyperlink" href="https://www.portainer.io/business-upsell?from=restore-s3-form" target="_blank"> Portainer Business Edition</a></p
>
</label>
</div> </div>
</div> </div>
<!-- note --> <!-- note -->
@@ -21,17 +21,19 @@
</div> </div>
<!-- !note --> <!-- !note -->
<!-- environment-type --> <!-- environment-type -->
<div class="form-group" style="margin-bottom: 0"> <div class="form-group">
<div class="boxselector_wrapper"> <div class="col-sm-12">
<div ng-repeat="type in ctrl.endpointSections"> <div class="boxselector_wrapper">
<input type="radio" id="{{ type.Id }}" ng-model="ctrl.formValues.ConnectionType" ng-value="type.Value" /> <div ng-repeat="type in ctrl.endpointSections">
<label for="{{ type.Id }}"> <input type="radio" id="{{ type.Id }}" ng-model="ctrl.formValues.ConnectionType" ng-value="type.Value" />
<div class="boxselector_header"> <label for="{{ type.Id }}">
<i ng-class="type.Classes" aria-hidden="true" style="margin-right: 2px"></i> <div class="boxselector_header">
{{ type.Title }} <i ng-class="type.Classes" aria-hidden="true" style="margin-right: 2px"></i>
</div> {{ type.Title }}
<p>{{ type.Description }}</p> </div>
</label> <p>{{ type.Description }}</p>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -7,11 +7,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="col-sm-12 form-section-title"> Registry provider </div> <div class="col-sm-12 form-section-title"> Registry provider </div>
<div class="form-group"></div> <box-selector radio-name="'availableRegistry'" value="$ctrl.state.registryValue" options="$ctrl.state.availableRegistry" on-change="($ctrl.setRegistry)"></box-selector>
<div class="form-group" style="margin-bottom: 0">
<box-selector radio-name="'registry'" value="$ctrl.state.registryValue" options="$ctrl.state.availableRegistry" on-change="($ctrl.setRegistry)"></box-selector>
</div>
<registry-form-quay <registry-form-quay
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.QUAY" ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.QUAY"
@@ -1,10 +1,13 @@
import { Edit } from 'react-feather';
import Docker from '@/assets/ico/vendor/docker.svg?c'; import Docker from '@/assets/ico/vendor/docker.svg?c';
import Ecr from '@/assets/ico/vendor/ecr.svg?c'; import Ecr from '@/assets/ico/vendor/ecr.svg?c';
import Quay from '@/assets/ico/vendor/quay.svg?c'; import Quay from '@/assets/ico/vendor/quay.svg?c';
import Proget from '@/assets/ico/vendor/proget.svg?c'; import Proget from '@/assets/ico/vendor/proget.svg?c';
import Azure from '@/assets/ico/vendor/azure.svg?c'; import Azure from '@/assets/ico/vendor/azure.svg?c';
import Gitlab from '@/assets/ico/vendor/gitlab.svg?c'; import Gitlab from '@/assets/ico/vendor/gitlab.svg?c';
import Custom from '@/assets/ico/custom.svg?c';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
export const options = [ export const options = [
{ {
@@ -51,7 +54,7 @@ export const options = [
}, },
{ {
id: 'registry_custom', id: 'registry_custom',
icon: Custom, icon: <BadgeIcon icon={Edit} />,
label: 'Custom registry', label: 'Custom registry',
description: 'Define your own registry', description: 'Define your own registry',
value: '3', value: '3',
@@ -1,12 +1,16 @@
import { ArrowDownCircle } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums'; import { FeatureId } from '@/portainer/feature-flags/enums';
import Microsoft from '@/assets/ico/vendor/microsoft.svg?c'; import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
import Ldap from '@/assets/ico/ldap.svg?c'; import Ldap from '@/assets/ico/ldap.svg?c';
import Oauth from '@/assets/ico/oauth.svg?c'; import OAuth from '@/assets/ico/oauth.svg?c';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
export const options = [ export const options = [
{ {
id: 'auth_internal', id: 'auth_internal',
icon: 'svg-internal', icon: <BadgeIcon icon={ArrowDownCircle} />,
label: 'Internal', label: 'Internal',
description: 'Internal authentication mechanism', description: 'Internal authentication mechanism',
value: 1, value: 1,
@@ -28,7 +32,7 @@ export const options = [
}, },
{ {
id: 'auth_oauth', id: 'auth_oauth',
icon: Oauth, icon: OAuth,
label: 'OAuth', label: 'OAuth',
description: 'OAuth authentication', description: 'OAuth authentication',
value: 3, value: 3,

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