Compare commits

...

38 Commits

Author SHA1 Message Date
itsconquest
103fbbfe6e fix(boxselector): fix darkmode BE teaser style [EE-4145] (#7597)
* 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

View File

@@ -5,17 +5,17 @@ import (
"errors"
"fmt"
"net/http"
netUrl "net/url"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(url string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
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 {
return 0, "", err
}

View File

@@ -259,9 +259,6 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
endpointType := portainer.DockerEnvironment
var agentVersion string
if payload.EndpointCreationType == agentEnvironment {
payload.URL = "tcp://" + normalizeAgentAddress(payload.URL)
var tlsConfig *tls.Config
if payload.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)

View File

@@ -105,12 +105,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.URL != nil {
if endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.AgentOnKubernetesEnvironment {
endpoint.URL = normalizeAgentAddress(*payload.URL)
} else {
endpoint.URL = *payload.URL
}
endpoint.URL = *payload.URL
}
if payload.PublicURL != nil {

View File

@@ -1,18 +1,6 @@
package endpoints
import "strings"
func BoolAddr(b bool) *bool {
boolVar := b
return &boolVar
}
func normalizeAgentAddress(url string) string {
// Case insensitive strip http or https scheme if URL entered
index := strings.Index(url, "://")
if index >= 0 {
return url[index+3:]
}
return url
}

View File

@@ -181,6 +181,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
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
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):

View File

@@ -5,14 +5,13 @@ import (
"log"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/agent"
"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
@@ -34,7 +33,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := parseURL(urlString)
endpointURL, err := url.ParseURL(urlString)
if err != nil {
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
}
@@ -99,15 +98,3 @@ func (proxy *ProxyServer) 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)
}

View File

@@ -5,13 +5,13 @@ import (
"io"
"log"
"net/http"
"net/url"
"strings"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"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) {
@@ -23,7 +23,7 @@ func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.
}
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 {
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)
}
endpointURL, err := url.Parse(rawURL)
endpointURL, err := url.ParseURL(rawURL)
if err != nil {
return nil, err
}

19
api/internal/url/url.go Normal file
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)
}

View File

@@ -52,7 +52,11 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
d.swarmStackManager.Login(registries, 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 {

View File

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

View File

@@ -117,10 +117,6 @@ input[type='checkbox'] {
text-align: center;
}
a[ng-click] {
cursor: pointer;
}
.space-right {
margin-right: 5px;
}
@@ -481,7 +477,7 @@ a[ng-click] {
:root[theme='dark'] .bootbox-checkbox-list,
:root[theme='highcontrast'] .bootbox-checkbox-list {
background-color: var(--black-color);
background-color: var(--bg-modal-content-color);
}
.small-select {

View File

@@ -12,8 +12,11 @@
}
.control-label {
color: var(--ui-gray-7);
font-weight: 500;
@apply inline-flex items-center;
@apply font-medium;
@apply text-gray-7;
@apply th-dark:text-gray-warm-3;
@apply th-highcontrast:text-white;
}
.vertical-center {
@@ -68,6 +71,10 @@
.switch input[type='checkbox']:disabled + .slider {
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 {
@@ -88,6 +95,8 @@
background-color: var(--bg-switch-box-color);
-webkit-transition: 0.4s;
transition: 0.4s;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
}
.slider:before {
@@ -104,6 +113,8 @@
input:checked + .slider {
background-color: var(--ui-blue-8);
@apply th-dark:bg-blue-9;
@apply th-highcontrast:bg-blue-9;
}
input:focus + .slider {
@@ -355,10 +366,6 @@ input:checked + .slider:before {
color: var(--ui-error-9);
}
.control-label {
@apply inline-flex items-center;
}
.progress {
height: 8px;
border-radius: 4px;

View File

@@ -25,14 +25,14 @@ fieldset[disabled] .btn {
box-shadow: none;
}
.btn-primary {
.btn.btn-primary {
@apply text-white bg-blue-8 border-blue-8;
@apply hover:text-white hover:bg-blue-9 hover:border-blue-9;
@apply th-dark:hover:bg-blue-7 th-dark:hover:border-blue-7;
}
.btn-primary:active,
.btn-primary.active,
.btn.btn-primary:active,
.btn.btn-primary.active,
.open > .dropdown-toggle.btn-primary {
@apply bg-blue-9 border-blue-5;
}
@@ -44,7 +44,7 @@ fieldset[disabled] .btn {
}
/* Button Secondary */
.btn-secondary {
.btn.btn-secondary {
@apply border border-solid;
@apply text-blue-9 bg-blue-2 border-blue-8;
@@ -54,18 +54,18 @@ fieldset[disabled] .btn {
@apply th-dark:hover:bg-blue-11;
}
.btn-danger {
.btn.btn-danger {
@apply bg-error-8 border-error-8;
@apply hover:bg-error-7 hover:border-error-7 hover:text-white;
}
.btn-danger:active,
.btn-danger.active,
.btn.btn-danger:active,
.btn.btn-danger.active,
.open > .dropdown-toggle.btn-danger {
@apply bg-error-8 text-white border-blue-5;
}
.btn-dangerlight {
.btn.btn-dangerlight {
@apply text-error-9 th-dark:text-white;
@apply bg-error-3 th-dark:bg-error-9;
@apply hover:bg-error-2 th-dark:hover:bg-error-11;
@@ -73,39 +73,38 @@ fieldset[disabled] .btn {
@apply border border-solid;
}
.btn-success {
.btn.btn-success {
background-color: var(--ui-success-7);
}
.btn-success:hover {
.btn.btn-success:hover {
color: var(--white-color);
}
/* secondary-grey */
.btn-default,
.btn-light {
.btn.btn-default,
.btn.btn-light {
@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;
@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-light.active,
.btn.btn-light:active,
.btn.btn-light.active,
.open > .dropdown-toggle.btn-light {
background-color: var(--ui-gray-3);
}
.hyperlink,
.hyperlink:focus {
color: var(--ui-blue-8);
}
.hyperlink:hover {
text-decoration: underline;
color: var(--ui-blue-9);
.btn.btn-link {
@apply text-blue-8 hover:text-blue-9 disabled:text-gray-5;
@apply th-dark:text-blue-8 th-dark:hover:text-blue-7;
@apply th-highcontrast:text-blue-8 th-highcontrast:hover:text-blue-7;
}
.btn-group {
@@ -120,30 +119,43 @@ fieldset[disabled] .btn {
/* focus */
.btn-primary:focus,
.btn-secondary:focus,
.btn-light:focus {
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus {
@apply border-blue-5;
}
.btn-danger:focus,
.btn-dangerlight:focus {
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
@apply border-blue-6;
}
.btn-primary:focus,
.btn-secondary:focus,
.btn-light:focus,
.btn-danger:focus,
.btn-dangerlight:focus {
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus,
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-3);
box-shadow: 0px 0px 0px 4px var(--btn-focus-color);
}
[theme='dark'] .btn-primary:focus,
[theme='dark'] .btn-secondary:focus,
[theme='dark'] .btn-light:focus,
[theme='dark'] .btn-danger:focus,
[theme='dark'] .btn-dangerlight:focus {
[theme='dark'] .btn.btn-primary:focus,
[theme='dark'] .btn.btn-secondary:focus,
[theme='dark'] .btn.btn-light:focus,
[theme='dark'] .btn.btn-danger:focus,
[theme='dark'] .btn.btn-dangerlight:focus {
--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;
}

View File

@@ -31,17 +31,6 @@
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 {
color: var(--text-input-group-addon-color);
background-color: var(--bg-input-group-addon-color);

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">
<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 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"/>
</svg>
<svg width="auto" height="auto" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
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>

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 604 B

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -31,7 +31,7 @@ export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube
export const PORTAINER_FADEOUT = 1500;
export const STACK_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';
// don't declare new constants, either:
@@ -66,4 +66,5 @@ angular
.constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS)
.constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY)
.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);

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 = {
name: 'docker.stacks.newstack',
url: '/newstack',
@@ -553,6 +564,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
$stateRegistryProvider.register(serviceLogs);
$stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackContainer);
$stateRegistryProvider.register(stackCreation);
$stateRegistryProvider.register(swarm);
$stateRegistryProvider.register(swarmVisualizer);

View File

@@ -50,9 +50,9 @@
ng-click="$ctrl.expandItem(value, !value.Expanded)"
>
<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>
</a>
</button>
<a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a>
</td>
<td>{{ value.IPAddress || '-' }}</td>

View File

@@ -127,15 +127,20 @@
<thead>
<tr>
<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">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Id')">
Id
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
<div class="flex gap-1 items-center">
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<table-column-header
col-title="'Id'"
can-sort="true"
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>
<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>
@@ -160,32 +165,40 @@
</div>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RepoTags')">
Tags
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RepoTags' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Tags'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'RepoTags'"
is-sorted-desc="$ctrl.state.orderBy === 'RepoTags' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('RepoTags')"
></table-column-header>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('VirtualSize')">
Size
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'VirtualSize' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Size'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'VirtualSize'"
is-sorted-desc="$ctrl.state.orderBy === 'VirtualSize' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('VirtualSize')"
></table-column-header>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Created')">
Created
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Created'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Created'"
is-sorted-desc="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Created')"
></table-column-header>
</th>
<th ng-if="$ctrl.showHostColumn">
<a ng-click="$ctrl.changeOrderBy('NodeName')">
Host
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Host'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'NodeName'"
is-sorted-desc="$ctrl.state.orderBy === 'NodeName' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('NodeName')"
></table-column-header>
</th>
</tr>
</thead>

View File

@@ -156,7 +156,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event); $event.stopPropagation()" />
<label for="select_{{ $index }}"></label>
</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>
<a ui-sref="docker.services.service({id: item.Id})" ng-click="$event.stopPropagation()">{{ item.Name }}</a>

View File

@@ -190,11 +190,17 @@
item.Id | truncate: 40
}}</a>
<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">
<pr-icon icon="'search'" feather="true"></pr-icon> browse
</a>
</btn>
<button
ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode"
type="button"
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>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>

View File

@@ -33,7 +33,7 @@
<tr>
<td>
<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>
Node Labels
</td>

View File

@@ -31,7 +31,7 @@
</div>
</div>
<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">
<select class="form-control" ng-model="$ctrl.sinceTimestamp" id="logs_since">
<option selected value="">All logs</option>
@@ -40,20 +40,20 @@
</div>
</div>
<div class="form-group">
<label for="logs_search" class="col-sm-1 control-label text-left"> Search </label>
<div class="col-sm-11">
<label for="logs_search" class="col-sm-2 control-label text-left"> Search </label>
<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..." />
</div>
</div>
<div class="form-group">
<label for="lines_count" class="col-sm-1 control-label text-left"> Lines </label>
<div class="col-sm-11">
<label for="lines_count" class="col-sm-2 control-label text-left"> Lines </label>
<div class="col-sm-8">
<input class="form-control" type="number" name="lines_count" ng-model="$ctrl.lineCount" placeholder="Enter no of lines..." />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.copySupported">
<label class="col-sm-1 control-label text-left"> Actions </label>
<div class="col-sm-11">
<label class="col-sm-2 control-label text-left"> Actions </label>
<div class="col-sm-10">
<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
>

View File

@@ -812,8 +812,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
function (d) {
var containers = d;
$scope.runningContainers = containers;
$scope.gpuUseAll = $scope.endpoint.Snapshots[0].GpuUseAll;
$scope.gpuUseList = $scope.endpoint.Snapshots[0].GpuUseList;
$scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
$scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
if ($transition$.params().from) {
loadFromContainerSpec();
} else {

View File

@@ -640,7 +640,7 @@
</div>
<!-- !runtimes -->
</form>
<form class="form-horizontal" style="margin-top: 15px">
<form class="form-horizontal" style="margin-top: 15px" name="resourceForm">
<!-- devices -->
<div ng-if="showDeviceMapping" class="form-group">
<div class="col-sm-12" style="margin-top: 5px">
@@ -712,9 +712,9 @@
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
<div class="col-sm-12 form-section-title"> Resources </div>
<!-- memory-reservation-input -->
<div class="form-group">
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory reservation </label>
<div class="col-sm-3">
<div class="form-group flex">
<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-6">
<slider
on-change="(handleResourceChange)"
model="formValues.MemoryReservation"
@@ -724,18 +724,34 @@
ng-if="state.sliderMaxMemory"
></slider>
</div>
<div class="col-sm-2">
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" />
<div class="col-sm-2 vertical-center">
<input
name="memory_reservation"
type="number"
min="0"
max="{{ state.sliderMaxMemory }}"
class="form-control"
ng-model="formValues.MemoryReservation"
id="memory-reservation"
required
/>
</div>
<div class="col-sm-4">
<p class="small text-muted mt-2"> Memory soft limit (<b>MB</b>) </p>
</div>
<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>
<!-- !memory-reservation-input -->
<!-- memory-limit-input -->
<div class="form-group">
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory limit </label>
<div class="col-sm-3">
<div class="form-group flex">
<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-6">
<slider
on-change="(handleResourceChange)"
model="formValues.MemoryLimit"
@@ -745,31 +761,44 @@
ng-if="state.sliderMaxMemory"
></slider>
</div>
<div class="col-sm-2">
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryLimit" id="memory-limit" />
<div class="col-sm-2 vertical-center">
<input
name="memory_Limit"
type="number"
min="0"
max="{{ state.sliderMaxMemory }}"
class="form-control"
ng-model="formValues.MemoryLimit"
id="memory-limit"
required
/>
</div>
<div class="col-sm-4">
<p class="small text-muted mt-7"> Memory limit (<b>MB</b>) </p>
</div>
<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>
<!-- !memory-limit-input -->
<!-- cpu-limit-input -->
<div class="form-group">
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> CPU limit </label>
<div class="col-sm-5">
<div class="form-group flex">
<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-8">
<slider
on-change="(handleResourceChange)"
model="formValues.CpuLimit"
floor="0"
ceil="state.sliderMaxCpu"
step="0.25"
step="0.1"
precision="2"
ng-if="state.sliderMaxCpu"
></slider>
</div>
<div class="col-sm-4 mt-8">
<p class="small text-muted"> Maximum CPU usage </p>
</div>
</div>
<!-- !cpu-limit-input -->

View File

@@ -65,7 +65,7 @@
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td class="col-xs-6 col-sm-4 col-md-3 col-lg-3">ID</td>
<td>{{ container.Id }}</td>
</tr>
<tr>

View File

@@ -77,29 +77,29 @@
</div>
<div class="dashboard-grid mx-4">
<a ui-sref="docker.stacks" ng-if="showStacks">
<dashboard-item feather-icon="true" icon="'layers'" feather-icon="true" type="'Stack'" value="stackCount"></dashboard-item>
<a class="no-link" ui-sref="docker.stacks" ng-if="showStacks">
<dashboard-item feather-icon="true" icon="'layers'" type="'Stack'" value="stackCount"></dashboard-item>
</a>
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="docker.services">
<a class="no-link" ui-sref="docker.services">
<dashboard-item feather-icon="true" icon="'shuffle'" type="'Service'" value="serviceCount"></dashboard-item>
</a>
</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>
</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>
</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>
</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>
</a>

View File

@@ -7,8 +7,8 @@
<form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title"> Host and Filesystem </div>
<div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
<span class="col-sm-12 text-muted small vertical-center">
<pr-icon icon="'info'" feather="true" mode="'primary'" class-name="space-right"></pr-icon>
These features are only available for an Agent enabled environments.
</span>
</div>

View File

@@ -76,37 +76,39 @@
<!-- build-method -->
<div class="col-sm-12 form-section-title"> Build method </div>
<div class="form-group"></div>
<div class="form-group" class="mb-0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()" />
<label for="method_editor">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()" />
<label for="method_upload">
<div class="boxselector_header vertical-center">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload a tarball or a Dockerfile from your computer</p>
</label>
</div>
<div>
<input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()" />
<label for="method_url">
<div class="boxselector_header vertical-center">
<pr-icon icon="'globe'" feather="true"></pr-icon>
URL
</div>
<p>Specify a URL to a file</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()" />
<label for="method_editor">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()" />
<label for="method_upload">
<div class="boxselector_header vertical-center">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload a tarball or a Dockerfile from your computer</p>
</label>
</div>
<div>
<input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()" />
<label for="method_url">
<div class="boxselector_header vertical-center">
<pr-icon icon="'globe'" feather="true"></pr-icon>
URL
</div>
<p>Specify a URL to a file</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -10,7 +10,9 @@
<div class="row">
<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">
<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 style="margin: 0px 5px" authorization="DockerImagePush">
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Push to registry" ng-click="pushTag(tag)">

View File

@@ -7,7 +7,7 @@
<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>
</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>
<table class="table" style="margin-top: 5px">
<thead>

View File

@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Container labels">
<div class="nopadding" authorization="DockerServiceUpdate">
<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>
</div>
</rd-widget-header>

View File

@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Environment variables">
<div class="nopadding" authorization="DockerServiceUpdate">
<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>
</div>
</rd-widget-header>

View File

@@ -10,7 +10,7 @@
<option value="none">none</option>
</select>
<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>
</div>
<table class="table">

View File

@@ -3,7 +3,7 @@
<rd-widget-header icon="list" feather-icon="true" title-text="Mounts">
<div class="nopadding" authorization="DockerServiceUpdate">
<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>
</div>
</rd-widget-header>

View File

@@ -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="true">Override</label>
</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>
<table class="table" style="margin-top: 5px">
<thead>

View File

@@ -34,27 +34,29 @@
<!-- edge-job-method-select -->
<div class="col-sm-12 form-section-title"> Edge job configuration </div>
<div class="form-group"></div>
<div class="form-group px-4">
<div class="boxselector_wrapper !mt-0">
<div>
<input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic" />
<label for="config_basic">
<div class="boxselector_header vertical-center">
<pr-icon icon="'calendar'" feather="true"></pr-icon>
Basic configuration
</div>
<p>Select date from calendar</p>
</label>
</div>
<div>
<input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced" />
<label for="config_advanced">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Advanced configuration
</div>
<p>Write your own cron rule</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper !mt-0">
<div>
<input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic" />
<label for="config_basic">
<div class="boxselector_header vertical-center">
<pr-icon icon="'calendar'" feather="true"></pr-icon>
Basic configuration
</div>
<p>Select date from calendar</p>
</label>
</div>
<div>
<input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced" />
<label for="config_advanced">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Advanced configuration
</div>
<p>Write your own cron rule</p>
</label>
</div>
</div>
</div>
</div>
@@ -151,28 +153,29 @@
<!-- execution-method -->
<div ng-if="!$ctrl.model.Id">
<div class="col-sm-12 form-section-title"> Job content </div>
<div class="form-group"></div>
<div class="form-group px-4">
<div class="boxselector_wrapper !mt-0">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.formValues.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.formValues.method" value="upload" />
<label for="method_upload">
<div class="boxselector_header vertical-center">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload from your computer</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.formValues.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header vertical-center">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.formValues.method" value="upload" />
<label for="method_upload">
<div class="boxselector_header vertical-center">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload from your computer</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -1,13 +1,15 @@
import { compose, kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
export default class EdgeStackDeploymentTypeSelectorController {
/* @ngInject */
constructor() {
this.deploymentOptions = [
{ id: 'deployment_compose', icon: 'fab fa-docker', label: 'Compose', description: 'Docker compose format', value: 0 },
{
id: 'deployment_kube',
icon: 'fa fa-cubes',
label: 'Kubernetes',
description: 'Kubernetes manifest format',
...compose,
value: 0,
},
{
...kubernetes,
value: 1,
disabled: () => {
return this.hasDockerEndpoint();

View File

@@ -26,27 +26,29 @@
</div>
<div class="col-sm-12 form-section-title"> Group type </div>
<div class="col-sm-12 !px-0">
<div class="boxselector_wrapper">
<div class="boxselector">
<input type="radio" id="static-group" ng-model="$ctrl.model.Dynamic" ng-value="false" ng-checked="!$ctrl.model.Dynamic" />
<label for="static-group">
<div class="boxselector_header vertical-center">
<pr-icon icon="'list'" feather="true"></pr-icon>
Static
</div>
<p>Manually select Edge environments</p>
</label>
</div>
<div class="boxselector">
<input type="radio" id="dynamic-group" ng-model="$ctrl.model.Dynamic" ng-value="true" ng-checked="$ctrl.model.Dynamic" />
<label for="dynamic-group">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true" className="'feather'"></pr-icon>
Dynamic
</div>
<p>Automatically associate environments via tags</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div class="boxselector">
<input type="radio" id="static-group" ng-model="$ctrl.model.Dynamic" ng-value="false" ng-checked="!$ctrl.model.Dynamic" />
<label for="static-group">
<div class="boxselector_header vertical-center">
<pr-icon icon="'list'" feather="true"></pr-icon>
Static
</div>
<p>Manually select Edge environments</p>
</label>
</div>
<div class="boxselector">
<input type="radio" id="dynamic-group" ng-model="$ctrl.model.Dynamic" ng-value="true" ng-checked="$ctrl.model.Dynamic" />
<label for="dynamic-group">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true"></pr-icon>
Dynamic
</div>
<p>Automatically associate environments via tags</p>
</label>
</div>
</div>
</div>
</div>
@@ -78,27 +80,29 @@
<!-- DynamicGroup -->
<div ng-if="$ctrl.model.Dynamic">
<div class="col-sm-12 form-section-title"> Tags </div>
<div ng-if="$ctrl.tags.length" class="form-group col-sm-12">
<div class="boxselector_wrapper">
<div class="boxselector">
<input type="radio" id="or-selector" ng-model="$ctrl.model.PartialMatch" ng-value="true" ng-checked="$ctrl.model.PartialMatch" />
<label for="or-selector">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true"></pr-icon>
Partial match
</div>
<p>Associate any environment matching at least one of the selected tags</p>
</label>
</div>
<div class="boxselector">
<input type="radio" id="and-selector" ng-model="$ctrl.model.PartialMatch" ng-value="false" ng-checked="!$ctrl.model.PartialMatch" />
<label for="and-selector">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true" className="'feather'"></pr-icon>
Full match
</div>
<p>Associate any environment matching all of the selected tags</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div class="boxselector">
<input type="radio" id="or-selector" ng-model="$ctrl.model.PartialMatch" ng-value="true" ng-checked="$ctrl.model.PartialMatch" />
<label for="or-selector">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true"></pr-icon>
Partial match
</div>
<p>Associate any environment matching at least one of the selected tags</p>
</label>
</div>
<div class="boxselector">
<input type="radio" id="and-selector" ng-model="$ctrl.model.PartialMatch" ng-value="false" ng-checked="!$ctrl.model.PartialMatch" />
<label for="and-selector">
<div class="boxselector_header vertical-center">
<pr-icon icon="'tag'" feather="true"></pr-icon>
Full match
</div>
<p>Associate any environment matching all of the selected tags</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -1,14 +1,11 @@
import { editor, git, template, upload } from '@@/BoxSelector/common-options/build-methods';
class DockerComposeFormController {
/* @ngInject */
constructor($async, EdgeTemplateService, Notifications) {
Object.assign(this, { $async, EdgeTemplateService, Notifications });
this.methodOptions = [
{ 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.methodOptions = [editor, upload, git, template];
this.selectedTemplate = null;

View File

@@ -1,13 +1,11 @@
import { editor, git, upload } from '@@/BoxSelector/common-options/build-methods';
class KubeManifestFormController {
/* @ngInject */
constructor($async) {
Object.assign(this, { $async });
this.methodOptions = [
{ 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.methodOptions = [editor, upload, git];
this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);

View File

@@ -113,7 +113,7 @@
</div>
</div>
<!-- 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">
<thead ng-class="{ 'secondary-heading': !$ctrl.isPrimary }">
<tr role="row">
@@ -238,13 +238,18 @@
</div>
</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 }}
</a>
<a
ng-if="!item.KubernetesApplications"
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })"
ng-click="$event.stopPropagation()"
class="hyperlink"
>{{ item.Name }}
</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>

View File

@@ -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="input-group input-group-sm">
<span class="input-group-addon">Route</span>
<span class="input-group-addon required">Route</span>
<input
class="form-control"
name="ingress_route_{{ $index }}"

View File

@@ -3,14 +3,12 @@
<div class="form-group" ng-if="$ctrl.isCreation">
<div class="col-sm-12">
<p>
<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
</a>
<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
</a>
</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()">
<pr-icon icon="'list'" feather="true"></pr-icon> Advanced mode
</button>
<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()">
<pr-icon icon="'edit'" feather="true"></pr-icon> Simple mode
</button>
</div>
<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>

View File

@@ -1,17 +1,14 @@
import { buildOption } from '@/portainer/components/BoxSelector';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
class KubeCreateCustomTemplateViewController {
/* @ngInject */
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService });
this.methodOptions = [
buildOption('method_editor', 'svg-custom', 'Web editor', 'Use our Web editor', 'editor'),
buildOption('method_upload', 'svg-upload', 'Upload', 'Upload from your computer', 'upload'),
];
this.methodOptions = [editor, upload];
this.templates = null;
this.isTemplateVariablesEnabled = isBE;

View File

@@ -145,7 +145,7 @@
</div>
<div class="form-group" ng-show="kubernetesApplicationCreationForm.application_name.$invalid || ctrl.state.alreadyExists">
<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">
<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
@@ -156,10 +156,12 @@
alphanumeric character (e.g. 'my-name', or 'abc-123').
</p>
</div>
<p class="text-muted vertical-center" ng-if="ctrl.state.alreadyExists">
<pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
An application with the same name already exists inside the selected namespace.
</p>
<div class="col-sm-8" ng-if="ctrl.state.alreadyExists">
<p class="text-muted vertical-center">
<pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
An application with the same name already exists inside the selected namespace.
</p>
</div>
</div>
</div>
<!-- #endregion -->
@@ -709,83 +711,85 @@
</div>
<!-- access policy options -->
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper !px-[15px]">
<div
ng-if="
(!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="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div
ng-if="
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.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 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"
<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
"
>
<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>
<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">
<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>
@@ -897,56 +901,58 @@
</div>
<!-- deployment options -->
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper !px-[15px]">
<div>
<input
type="radio"
id="deployment_replicated"
ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED"
ng-model="ctrl.formValues.DeploymentType"
data-cy="k8sAppCreate-replicatedDeploymentButton"
/>
<label for="deployment_replicated">
<div class="boxselector_header">
<pr-icon icon="'sliders'" feather="true"></pr-icon>
Replicated
</div>
<p>Run one or multiple instances of this container</p>
</label>
</div>
<div ng-if="!ctrl.supportGlobalDeployment()">
<input type="radio" id="deployment_global" disabled />
<label
for="deployment_global"
tooltip-append-to-body="true"
tooltip-placement="bottom"
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>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
<div ng-if="ctrl.supportGlobalDeployment()">
<input
type="radio"
id="deployment_global"
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-model="ctrl.formValues.DeploymentType"
ng-click="ctrl.unselectAutoScaler()"
data-cy="k8sAppCreate-globalDeployButton"
/>
<label for="deployment_global">
<div class="boxselector_header">
<pr-icon icon="'svg-cubes'"></pr-icon>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the sdfh</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input
type="radio"
id="deployment_replicated"
ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED"
ng-model="ctrl.formValues.DeploymentType"
data-cy="k8sAppCreate-replicatedDeploymentButton"
/>
<label for="deployment_replicated">
<div class="boxselector_header">
<pr-icon icon="'sliders'" feather="true"></pr-icon>
Replicated
</div>
<p>Run one or multiple instances of this container</p>
</label>
</div>
<div ng-if="!ctrl.supportGlobalDeployment()">
<input type="radio" id="deployment_global" disabled />
<label
for="deployment_global"
tooltip-append-to-body="true"
tooltip-placement="bottom"
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>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
<div ng-if="ctrl.supportGlobalDeployment()">
<input
type="radio"
id="deployment_global"
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-model="ctrl.formValues.DeploymentType"
ng-click="ctrl.unselectAutoScaler()"
data-cy="k8sAppCreate-globalDeployButton"
/>
<label for="deployment_global">
<div class="boxselector_header">
<pr-icon icon="'svg-cubes'"></pr-icon>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the sdfh</p>
</label>
</div>
</div>
</div>
</div>
@@ -1055,90 +1061,96 @@
</div>
<div class="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed">
<table class="table" style="margin-bottom: 0px">
<tbody>
<tr class="small">
<td style="width: 33%; border: none; padding: 2px 0 2px 0">Minimum instances</td>
<td style="width: 33%; border: none; padding: 2px 0 2px 0">Maximum instances</td>
<td style="width: 33%; border: none; padding: 2px 0 2px 0">
Target CPU usage (<b>%</b>)
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
</portainer-tooltip>
</td>
</tr>
<tr>
<td style="padding: 8px 5px 5px 0; border: none">
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_min"
min="0"
ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
ng-model="ctrl.formValues.AutoScaler.MinReplicas"
data-cy="k8sAppCreate-autoScaleMin"
required
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances is required.</p>
<p ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be greater than 0.</p>
<p ng-message="max"
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be smaller than maximum instances.</p
>
</ng-messages>
</div>
</div>
</td>
<td style="padding: 8px 5px 5px 0; border: none">
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_max"
ng-min="ctrl.formValues.AutoScaler.MinReplicas"
ng-model="ctrl.formValues.AutoScaler.MaxReplicas"
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances is required.</p>
<p ng-message="min"
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances must be greater than minimum instances.</p
>
</ng-messages>
</div>
</div>
</td>
<td style="padding: 8px 5px 5px 0; border: none">
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_cpu"
ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization"
min="1"
max="100"
required
data-cy="k8sAppCreate-targetCPUInput"
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage is required.</p>
<p ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be greater than 0.</p>
<p ng-message="max"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be smaller than 100.</p>
</ng-messages>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_min">Minimum instances</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_min"
min="0"
ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
ng-model="ctrl.formValues.AutoScaler.MinReplicas"
data-cy="k8sAppCreate-autoScaleMin"
required
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid">
<div class="small text-muted" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error">
<p ng-message="required" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances is required.
</p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be greater than 0.
</p>
<p ng-message="max" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Minimum instances must be smaller than maximum instances.
</p>
</ng-messages>
</div>
</span>
</div>
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_max">Maximum instances</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_max"
ng-min="ctrl.formValues.AutoScaler.MinReplicas"
ng-model="ctrl.formValues.AutoScaler.MaxReplicas"
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()">
<div class="small text-muted" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error">
<p ng-message="required" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances is required.
</p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Maximum instances must be greater than minimum instances.
</p>
</ng-messages>
</div>
</span>
</div>
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_cpu">
Target CPU usage (<b>%</b>)
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
</portainer-tooltip>
</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_cpu"
ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization"
min="1"
max="100"
required
data-cy="k8sAppCreate-targetCPUInput"
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<div class="small text-muted" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error">
<p ng-message="required" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage is required.
</p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Target CPU usage must be greater than 0.
</p>
<p ng-message="max" class="vertical-center">
<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="col-sm-12 small text-danger">
@@ -1234,39 +1246,41 @@
</div>
<!-- placement policy options -->
<div class="form-group" style="margin-bottom: 0" ng-if="ctrl.formValues.Placements.length">
<div class="boxselector_wrapper !px-[15px]">
<div>
<input
type="radio"
id="placement_hard"
ng-value="ctrl.ApplicationPlacementTypes.MANDATORY"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-mandatoryPlacementButton"
/>
<label for="placement_hard">
<div class="boxselector_header">
<pr-icon icon="'sliders'" feather="true"></pr-icon>
Mandatory
</div>
<p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p>
</label>
</div>
<div>
<input
type="radio"
id="placement_soft"
ng-value="ctrl.ApplicationPlacementTypes.PREFERRED"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-prefferedPlacementButton"
/>
<label for="placement_soft">
<div class="boxselector_header">
<pr-icon icon="'align-justify'" feather="true"></pr-icon>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
<div class="form-group" ng-if="ctrl.formValues.Placements.length">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input
type="radio"
id="placement_hard"
ng-value="ctrl.ApplicationPlacementTypes.MANDATORY"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-mandatoryPlacementButton"
/>
<label for="placement_hard">
<div class="boxselector_header">
<pr-icon icon="'sliders'" feather="true"></pr-icon>
Mandatory
</div>
<p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p>
</label>
</div>
<div>
<input
type="radio"
id="placement_soft"
ng-value="ctrl.ApplicationPlacementTypes.PREFERRED"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-prefferedPlacementButton"
/>
<label for="placement_soft">
<div class="boxselector_header">
<pr-icon icon="'align-justify'" feather="true"></pr-icon>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -87,27 +87,29 @@
</div>
<!-- type options -->
<div class="form-group px-[15px]" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'svg-filecode'"></pr-icon>
ConfigMap
</div>
<p>This configuration holds non-sensitive information</p>
</label>
</div>
<div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'lock'" feather="true"></pr-icon>
Secret
</div>
<p>This configuration holds sensitive information</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'svg-filecode'"></pr-icon>
ConfigMap
</div>
<p>This configuration holds non-sensitive information</p>
</label>
</div>
<div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<div class="boxselector_header">
<pr-icon icon="'lock'" feather="true"></pr-icon>
Secret
</div>
<p>This configuration holds sensitive information</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -40,23 +40,23 @@
<div class="dashboard-grid mx-4">
<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>
</a>
</div>
<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>
</a>
</div>
<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>
</a>
</div>
<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>
</a>
</div>

View File

@@ -5,9 +5,10 @@ import uuidv4 from 'uuid/v4';
import PortainerError from '@/portainer/error';
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 { 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 {
/* @ngInject */
@@ -27,15 +28,15 @@ class KubernetesDeployController {
this.isTemplateVariablesEnabled = isBE;
this.deployOptions = [
buildOption('method_kubernetes', 'svg-kubernetes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
buildOption('method_compose', 'svg-dockercompose', 'Compose', 'Docker compose format', KubernetesDeployManifestTypes.COMPOSE),
{ ...kubernetes, value: KubernetesDeployManifestTypes.KUBERNETES },
{ ...compose, value: KubernetesDeployManifestTypes.COMPOSE },
];
this.methodOptions = [
buildOption('method_repo', 'svg-git', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT),
buildOption('method_editor', 'svg-custom', 'Web editor', 'Use our Web editor', KubernetesDeployBuildMethods.WEB_EDITOR),
buildOption('method_url', 'svg-url', 'URL', 'Specify a URL to a file', KubernetesDeployBuildMethods.URL),
buildOption('method_template', 'svg-template', 'Custom Template', 'Use a custom template', KubernetesDeployBuildMethods.CUSTOM_TEMPLATE),
{ ...git, value: KubernetesDeployBuildMethods.GIT },
{ ...editor, value: KubernetesDeployBuildMethods.WEB_EDITOR },
{ ...url, value: KubernetesDeployBuildMethods.URL },
{ ...template, value: KubernetesDeployBuildMethods.CUSTOM_TEMPLATE },
];
this.state = {

View File

@@ -42,6 +42,9 @@
<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
>
<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>
</span>

View File

@@ -47,16 +47,12 @@ export function EditDetails({
return (
<>
<div className="form-group">
<div className="col-sm-12">
<BoxSelector
radioName={withNamespace('ownership')}
value={values.ownership}
options={options}
onChange={(ownership) => handleChangeOwnership(ownership)}
/>
</div>
</div>
<BoxSelector
radioName={withNamespace('ownership')}
value={values.ownership}
options={options}
onChange={(ownership) => handleChangeOwnership(ownership)}
/>
{values.ownership === ResourceControlOwnership.RESTRICTED && (
<div aria-label="extra-options">

View File

@@ -6,6 +6,7 @@ import { ownershipIcon } from '@/portainer/filters/filters';
import { Team } from '@/portainer/teams/types';
import { BoxSelectorOption } from '@@/BoxSelector/types';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
import { ResourceControlOwnership } from '../types';
@@ -15,7 +16,7 @@ const publicOption: BoxSelectorOption<ResourceControlOwnership> = {
id: 'access_public',
description:
'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(
@@ -40,14 +41,14 @@ function adminOptions() {
return [
buildOption(
'access_administrators',
ownershipIcon('administrators'),
<BadgeIcon icon={ownershipIcon('administrators')} />,
'Administrators',
'I want to restrict the management of this resource to administrators only',
ResourceControlOwnership.ADMINISTRATORS
),
buildOption(
'access_restricted',
ownershipIcon('restricted'),
<BadgeIcon icon={ownershipIcon('restricted')} />,
'Restricted',
'I want to restrict the management of this resource to a set of users and/or teams',
ResourceControlOwnership.RESTRICTED
@@ -58,7 +59,7 @@ function nonAdminOptions(teams?: Team[]) {
return _.compact([
buildOption(
'access_private',
ownershipIcon('private'),
<BadgeIcon icon={ownershipIcon('private')} />,
'Private',
'I want to this resource to be manageable by myself only',
ResourceControlOwnership.PRIVATE
@@ -67,7 +68,7 @@ function nonAdminOptions(teams?: Team[]) {
teams.length > 0 &&
buildOption(
'access_restricted',
ownershipIcon('restricted'),
<BadgeIcon icon={ownershipIcon('restricted')} />,
'Restricted',
teams.length === 1
? `I want any member of my team (${teams[0].Name}) to be able to manage this resource`

View File

@@ -1,15 +1,16 @@
import { FeatureId } from '@/portainer/feature-flags/enums';
import { BoxSelectorOption } from '@@/BoxSelector/types';
import { IconProps } from '@@/Icon';
export function buildOption<T extends number | string>(
id: string,
icon: string,
icon: IconProps['icon'],
label: string,
description: string,
value: T,
feature?: FeatureId,
featherIcon?: boolean
featherIcon?: IconProps['featherIcon']
): BoxSelectorOption<T> {
return { id, icon, label, description, value, feature, featherIcon };
}

View File

@@ -15,51 +15,54 @@
</div>
<!-- !access-control-switch -->
<!-- restricted-access -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled" style="margin-bottom: 0">
<div class="boxselector_wrapper px-[15px]">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators" />
<label for="access_administrators" data-cy="portainer-selectAdminAccess">
<div class="boxselector_header">
<pr-icon icon="'eye-off'" feather="true"></pr-icon>
Administrators
</div>
<p class="boxselector_content">I want to restrict the management of this resource to administrators only</p>
</label>
</div>
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<label for="access_restricted" data-cy="portainer-selectRestrictedAccess">
<div class="boxselector_header">
<pr-icon icon="'users'" feather="true"></pr-icon>
Restricted
</div>
<p class="boxselector_content"> I want to restrict the management of this resource to a set of users and/or teams </p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin">
<input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private" />
<label for="access_private">
<div class="boxselector_header">
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
Private
</div>
<p> I want to this resource to be manageable by myself only </p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px"></i>
Restricted
</div>
<p ng-if="$ctrl.availableTeams.length === 1">
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b
>) to be able to manage this resource
</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 class="form-group" ng-if="$ctrl.formData.AccessControlEnabled">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators" />
<label for="access_administrators" data-cy="portainer-selectAdminAccess">
<div class="boxselector_header">
<pr-icon icon="'eye-off'" feather="true"></pr-icon>
Administrators
</div>
<p class="boxselector_content">I want to restrict the management of this resource to administrators only</p>
</label>
</div>
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<label for="access_restricted" data-cy="portainer-selectRestrictedAccess">
<div class="boxselector_header">
<pr-icon icon="'users'" feather="true"></pr-icon>
Restricted
</div>
<p class="boxselector_content"> I want to restrict the management of this resource to a set of users and/or teams </p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin">
<input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private" />
<label for="access_private">
<div class="boxselector_header">
<pr-icon icon="'eye-off'" feather="true"></pr-icon>
Private
</div>
<p> I want to this resource to be manageable by myself only </p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted" />
<label for="access_restricted">
<div class="boxselector_header">
<pr-icon icon="'users'" feather="true"></pr-icon>
Restricted
</div>
<p ng-if="$ctrl.availableTeams.length === 1">
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b
>) to be able to manage this resource
</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>

View File

@@ -12,7 +12,7 @@
<option value="" label="Select a Custom template" disabled selected="selected"> </option>
</select>
<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>
</div>
</div>

View File

@@ -40,19 +40,6 @@
@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 {
display: inline-flex;
}

View File

@@ -77,10 +77,12 @@
<input id="select_{{ $index }}" type="checkbox" disabled />
<label for="select_{{ $index }}"></label>
</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> docker.io </td>
<td> - </td>
</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))"

View File

@@ -24,47 +24,49 @@
</div>
<div class="form-group"></div>
<!-- endpoint-tls-mode -->
<div class="form-group" style="margin-bottom: 0" ng-if="$ctrl.formData.TLS">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca" />
<label for="tls_client_ca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with server and client verification
</div>
<p>Use client certificates and server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca" />
<label for="tls_client_noca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with client verification only
</div>
<p>Use client certificates without server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca" />
<label for="tls_ca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with server verification only
</div>
<p>Only verify the server certificate</p>
</label>
</div>
<div>
<input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only" />
<label for="tls_only">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS only
</div>
<p>No server/client verification</p>
</label>
<div class="form-group" ng-if="$ctrl.formData.TLS">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca" />
<label for="tls_client_ca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with server and client verification
</div>
<p>Use client certificates and server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca" />
<label for="tls_client_noca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with client verification only
</div>
<p>Use client certificates without server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca" />
<label for="tls_ca">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS with server verification only
</div>
<p>Only verify the server certificate</p>
</label>
</div>
<div>
<input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only" />
<label for="tls_only">
<div class="boxselector_header">
<pr-icon icon="'shield'" feather="true"></pr-icon>
TLS only
</div>
<p>No server/client verification</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,8 @@
class WebEditorFormController {
/* @ngInject */
constructor() {
constructor(BROWSER_OS_PLATFORM) {
this.editorUpdate = this.editorUpdate.bind(this);
this.BROWSER_OS_PLATFORM = BROWSER_OS_PLATFORM;
}
editorUpdate(cm) {

View File

@@ -1,7 +1,41 @@
<ng-form name="$ctrl.webEditorForm">
<div class="web-editor overflow-auto">
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title">Web editor</div>
<div class="trancluded-item form-group col-sm-12 col-lg-12 text-muted small" ng-transclude="description"></div>
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title pr-0"
>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="col-sm-12 col-lg-12">
<code-editor

View File

@@ -13,7 +13,9 @@
</div>
<div class="small vertical-center" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
<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 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>

View File

@@ -48,14 +48,24 @@
<div class="form-group">
<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>
Override default configuration
</a>
<a class="small interactive vertical-center" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.resetDefaults()">
</button>
<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>
Use default configuration
</a>
</button>
</div>
</div>

View File

@@ -103,12 +103,12 @@ class StackRedeployGitFormController {
async submit() {
const tplCrop =
'<div>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.</div>' +
'<div"><div style="position: absolute; right: 110px; top: 68px; z-index: 999">' +
'<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: 5px; top: 84px; z-index: 999">' +
'<be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div></div>';
const template = angular.element(tplCrop);
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) {
return;
}

View File

@@ -2,10 +2,8 @@
<rd-widget>
<rd-widget-header icon="sliders" feather-icon="true" title-text="User theme"></rd-widget-header>
<rd-widget-body>
<form class="theme-panel">
<!-- Theme Selector-->
<form class="form-horizontal">
<box-selector radio-name="'theme'" value="$ctrl.state.userTheme" options="$ctrl.state.availableThemes" on-change="($ctrl.setTheme)"></box-selector>
<!-- !Theme -->
</form>
<p class="mt-2 vertical-center">
<pr-icon icon="'alert-circle'" class-name="'icon-primary'" feather="true"></pr-icon>

View File

@@ -105,11 +105,11 @@ export interface EnvironmentOptions {
url?: string;
publicUrl?: string;
meta?: EnvironmentMetadata;
checkinInterval?: number;
azure?: AzureSettings;
tls?: TLSSettings;
isEdgeDevice?: boolean;
gpus?: Gpu[];
pollFrequency?: number;
}
interface CreateRemoteEnvironment {
@@ -130,7 +130,7 @@ export async function createRemoteEnvironment({
}: CreateRemoteEnvironment) {
return createEnvironment(name, creationType, {
...options,
url: `${url}`,
url: `tcp://${url}`,
});
}
@@ -175,6 +175,7 @@ export function createEdgeAgentEnvironment({
meta = { tagIds: [] },
gpus = [],
isEdgeDevice,
pollFrequency,
}: CreateEdgeAgentEnvironment) {
return createEnvironment(
name,
@@ -187,6 +188,7 @@ export function createEdgeAgentEnvironment({
},
gpus,
isEdgeDevice,
pollFrequency,
...meta,
}
);
@@ -211,7 +213,7 @@ async function createEnvironment(
PublicURL: options.publicUrl,
GroupID: groupId,
TagIds: arrayToJson(tagIds),
CheckinInterval: options.checkinInterval,
CheckinInterval: options.pollFrequency,
IsEdgeDevice: options.isEdgeDevice,
Gpus: arrayToJson(options.gpus),
};

View File

@@ -30,4 +30,5 @@ export enum FeatureId {
STACK_WEBHOOK = 'stack-webhook',
CONTAINER_WEBHOOK = 'container-webhook',
POD_SECURITY_POLICY_CONSTRAINT = 'pod-security-policy-constraint',
HIDE_DOCKER_HUB_ANONYMOUS = 'hide-docker-hub-anonymous',
}

View File

@@ -35,6 +35,7 @@ export async function init(edition: Edition) {
[FeatureId.STACK_WEBHOOK]: Edition.BE,
[FeatureId.CONTAINER_WEBHOOK]: Edition.BE,
[FeatureId.POD_SECURITY_POLICY_CONSTRAINT]: Edition.BE,
[FeatureId.HIDE_DOCKER_HUB_ANONYMOUS]: Edition.BE,
};
state.currentEdition = currentEdition;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import _ from 'lodash-es';
import filesize from 'filesize';
import { Eye, EyeOff, Users } from 'react-feather';
import { ResourceControlOwnership as RCO } from '@/portainer/access-control/types';
export function truncateLeftRight(text, max, left, right) {
@@ -106,13 +107,13 @@ export function environmentTypeIcon(type) {
export function ownershipIcon(ownership) {
switch (ownership) {
case RCO.PRIVATE:
return 'eye-off';
return EyeOff;
case RCO.ADMINISTRATORS:
return 'eye-off';
return EyeOff;
case RCO.RESTRICTED:
return 'users';
return Users;
default:
return 'eye';
return Eye;
}
}

View File

@@ -46,12 +46,11 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
<div className={styles.root}>
<button
type="button"
color="link"
onClick={() => onClick(environment)}
className={styles.wrapperButton}
>
<Link
className={clsx('blocklist-item', styles.item)}
className={clsx('blocklist-item no-link', styles.item)}
to={route}
params={{
endpointId: environment.Id,

View File

@@ -9,7 +9,6 @@
.refresh-environments-button {
margin-left: 0 !important;
padding: 8px 15px;
}
.filter-container {
@@ -73,7 +72,6 @@
.filterSearchbar {
display: inline-block;
width: 100%;
padding-left: 10px;
}
.filterSearchbar input[type='text'] {

View File

@@ -130,7 +130,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
status: statusFilter,
tagIds: tagFilter?.length ? tagFilter : undefined,
groupIds: groupFilter,
edgeDevice: getEdgeDeviceFilter(connectionTypes.map((p) => p.value)),
edgeDevice: false,
tagsPartialMatch: true,
agentVersions: agentVersions.map((a) => a.value),
};
@@ -192,8 +192,10 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
<Button
onClick={onRefresh}
data-cy="home-refreshEndpointsButton"
size="medium"
color="secondary"
className={clsx(
'vertical-center',
'vertical-center !ml-0',
styles.refreshEnvironmentsButton
)}
>
@@ -215,7 +217,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
}}
/>
</div>
<div className={styles.filterSearchbar}>
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
<FilterSearchBar
value={searchBarValue}
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(
platformTypes: PlatformType[],
connectionTypes: ConnectionType[]
@@ -495,7 +484,6 @@ function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
{ value: ConnectionType.API, label: 'API' },
{ value: ConnectionType.Agent, label: 'Agent' },
{ value: ConnectionType.EdgeAgent, label: 'Edge Agent' },
{ value: ConnectionType.EdgeDevice, label: 'Edge Device' },
];
if (platformTypes.length === 0) {

View File

@@ -1,3 +0,0 @@
.kubeconfig-button {
padding: 8px 15px;
}

View File

@@ -8,7 +8,6 @@ import { Query } from '@/portainer/environments/queries/useEnvironmentList';
import { Button } from '@@/buttons';
import styles from './KubeconfigButton.module.css';
import { KubeconfigPrompt } from './KubeconfigPrompt';
import '@reach/dialog/styles.css';
@@ -30,8 +29,8 @@ export function KubeconfigButton({ environments, envQueryParams }: Props) {
return (
<>
<Button className={styles.kubeconfigButton} onClick={handleClick}>
<Download className="feather-icon-white" aria-hidden="true" />{' '}
<Button onClick={handleClick} size="medium" className="!ml-3">
<Download className="feather icon-white" aria-hidden="true" />{' '}
Kubeconfig
</Button>
{prompt()}

View File

@@ -1,3 +1,5 @@
import { X } from 'react-feather';
import clsx from 'clsx';
import { useState } from 'react';
import { DialogOverlay } from '@reach/dialog';
@@ -59,7 +61,7 @@ export function KubeconfigPrompt({
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={onClose}>
×
<X />
</button>
<h5 className="modal-title">Download kubeconfig file</h5>
</div>
@@ -74,16 +76,24 @@ export function KubeconfigPrompt({
</div>
</form>
<br />
<Checkbox
id="settings-container-truncate-nae"
label="Select all (in this page)"
checked={isAllPageSelected}
onChange={handleSelectAll}
/>
<div className="h-8 flex items-center">
<Checkbox
id="settings-container-truncate-name"
label="Select all (in this page)"
checked={isAllPageSelected}
onChange={handleSelectAll}
/>
</div>
<div className="datatable">
<div className="bootbox-checkbox-list">
{environments.map((env) => (
<div className={styles.checkbox}>
<div
key={env.Id}
className={clsx(
styles.checkbox,
'h-8 flex items-center pt-1'
)}
>
<Checkbox
id={`${env.Id}`}
label={`${env.Name} (${env.URL})`}
@@ -95,7 +105,7 @@ export function KubeconfigPrompt({
</div>
))}
</div>
<div className="footer">
<div className="pt-3 flex justify-end w-full">
<PaginationControls
showAll={totalCount <= 100}
page={page}
@@ -111,7 +121,9 @@ export function KubeconfigPrompt({
<Button onClick={onClose} color="default">
Cancel
</Button>
<Button onClick={handleDownload}>Download File</Button>
<Button onClick={handleDownload} disabled={selectionSize < 1}>
Download File
</Button>
</div>
</div>
</div>
@@ -143,7 +155,7 @@ export function KubeconfigPrompt({
}
export function expiryMessage(expiry: string) {
const prefix = 'Kubeconfig file will';
const prefix = 'The kubeconfig file will';
switch (expiry) {
case '24h':
return `${prefix} expire in 1 day.`;

View File

@@ -5,6 +5,7 @@ import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
interface UIState {
dismissedInfoPanels: Record<string, string>;
dismissedInfoHash: string;
dismissedUpdateVersion: string;
}
type UIStateService = [UIState, (state: UIState) => void];

View File

@@ -36,6 +36,7 @@ export function PublicSettingsViewModel(settings) {
this.KubeconfigExpiry = settings.KubeconfigExpiry;
this.Features = settings.Features;
this.Edge = new EdgeSettingsViewModel(settings.Edge);
this.DefaultRegistry = settings.DefaultRegistry;
}
export function InternalAuthSettingsViewModel(data) {

View File

@@ -1,8 +1,11 @@
import { Edit } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums';
import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
import Google from '@/assets/ico/vendor/google.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 = [
{
@@ -32,7 +35,7 @@ export const options = [
},
{
id: 'custom',
icon: Custom,
icon: <BadgeIcon icon={Edit} />,
label: 'Custom',
description: 'Custom OAuth provider',
value: 'custom',

View File

@@ -15,6 +15,7 @@ import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell';
import { DashboardItem } from '@@/DashboardItem';
import { SearchBar } from '@@/datatables/SearchBar';
import { FallbackImage } from '@@/FallbackImage';
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
import { fileUploadField } from './file-upload-field';
import { switchField } from './switch-field';
@@ -28,7 +29,7 @@ export const componentsModule = angular
)
.component(
'portainerTooltip',
react2angular(Tooltip, ['message', 'position'])
react2angular(Tooltip, ['message', 'position', 'className'])
)
.component('fileUploadField', fileUploadField)
.component('porSwitchField', switchField)
@@ -49,7 +50,14 @@ export const componentsModule = angular
.component('viewLoading', r2a(ViewLoading, ['message']))
.component(
'pageHeader',
r2a(PageHeader, ['title', 'breadcrumbs', 'loading', 'onReload', 'reload'])
r2a(PageHeader, [
'id',
'title',
'breadcrumbs',
'loading',
'onReload',
'reload',
])
)
.component(
'fallbackImage',
@@ -76,4 +84,8 @@ export const componentsModule = angular
.component(
'datatableSearchbar',
r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder'])
)
.component(
'boxSelectorBadgeIcon',
react2angular(BadgeIcon, ['featherIcon', 'icon'])
).name;

View File

@@ -2,11 +2,19 @@ import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { CreateAccessToken } from '@/react/portainer/account/CreateAccessTokenView';
import {
DefaultRegistryAction,
DefaultRegistryDomain,
DefaultRegistryName,
} from '@/react/portainer/registries/ListView/DefaultRegistry';
import { wizardModule } from './wizard';
export const viewsModule = angular
.module('portainer.app.react.views', [wizardModule])
.component('defaultRegistryName', r2a(DefaultRegistryName, []))
.component('defaultRegistryAction', r2a(DefaultRegistryAction, []))
.component('defaultRegistryDomain', r2a(DefaultRegistryDomain, []))
.component(
'createAccessToken',
r2a(CreateAccessToken, ['onSubmit', 'onError'])

View File

@@ -8,7 +8,7 @@ import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/dialog/dialog.css';
import './codeMirrorDialog.css';
angular.module('portainer.app').factory('CodeMirrorService', function CodeMirrorService() {
'use strict';

View File

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

View File

@@ -167,8 +167,8 @@ export function confirmStackUpdate(
confirmButtonClassName: string | undefined,
callback: PromptCallback
) {
const sanitizedMessage = sanitize(message);
const sanitizedMessage =
typeof message === 'string' ? sanitize(message) : message;
const box = prompt({
title: buildTitle('Are you sure?'),
inputType: 'checkbox',
@@ -197,7 +197,8 @@ export function confirmStackUpdate(
'position: relative; display: block; margin-top: 10px; margin-bottom: 10px;'
);
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');
switchEle.prop('style', 'margin-left:20px');
}

View File

@@ -26,6 +26,7 @@ function StateManagerFactory(
dismissedInfoPanels: {},
dismissedInfoHash: '',
timesPasswordChangeSkipped: {},
dismissedUpdateVersion: '',
},
};

View File

@@ -109,7 +109,7 @@
</label>
<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" />
<span class="slider round"></span>
<span class="slider round before:content-['']"></span>
</label>
</div>
</div>

View File

@@ -1,6 +1,9 @@
import { Edit } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums';
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 = {
CUSTOM: 0,
@@ -11,7 +14,7 @@ const SERVER_TYPES = {
export const options = [
{
id: 'ldap_custom',
icon: Custom,
icon: <BadgeIcon icon={Edit} />,
label: 'Custom',
value: SERVER_TYPES.CUSTOM,
},

View File

@@ -12,8 +12,9 @@ import {
getSettings,
updateSettings,
getPublicSettings,
updateDefaultRegistry,
} from './settings.service';
import { Settings } from './types';
import { DefaultRegistry, Settings } from './types';
export function usePublicSettings<T = PublicSettingsViewModel>({
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')
)
);
}

View File

@@ -2,7 +2,7 @@ import { PublicSettingsViewModel } from '@/portainer/models/settings';
import axios, { parseAxiosError } from '../services/axios';
import { PublicSettingsResponse, Settings } from './types';
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
export async function getPublicSettings() {
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) {
let url = 'settings';
if (subResource) {

View File

@@ -89,6 +89,10 @@ enum AuthenticationMethod {
type Feature = string;
export interface DefaultRegistry {
Hide: boolean;
}
export interface Settings {
LogoURL: string;
BlackListedLabels: Pair[];

View File

@@ -8,7 +8,7 @@
</div>
Activity Logs
<be-feature-indicator feature="{{::$ctrl.feature}}"></be-feature-indicator>
<be-feature-indicator feature="$ctrl.feature"></be-feature-indicator>
</div>
<div class="vertical-center">
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar>

View File

@@ -8,7 +8,7 @@
</div>
Authentication Events
<be-feature-indicator feature="{{::$ctrl.feature}}"></be-feature-indicator>
<be-feature-indicator feature="$ctrl.feature"></be-feature-indicator>
</div>
<div class="vertical-center">
<datatable-searchbar on-change="($ctrl.onChangeKeyword)"></datatable-searchbar>

View File

@@ -15,38 +15,39 @@
<!-- build-method -->
<div ng-if="!$ctrl.state.fromStack">
<div class="col-sm-12 form-section-title"> Build method </div>
<div class="form-group"></div>
<div class="form-group mb-0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
<label for="method_upload">
<div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload from your computer</p>
</label>
</div>
<div>
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
<label for="method_repository">
<div class="boxselector_header">
<pr-icon icon="'git-pull-request'" feather="true"></pr-icon>
Repository
</div>
<p>Use a git repository</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
<label for="method_upload">
<div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload
</div>
<p>Upload from your computer</p>
</label>
</div>
<div>
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
<label for="method_repository">
<div class="boxselector_header">
<pr-icon icon="'git-pull-request'" feather="true"></pr-icon>
Repository
</div>
<p>Use a git repository</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -16,18 +16,19 @@
<!-- !name-input -->
<!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -17,17 +17,19 @@
<!-- build-method -->
<div class="col-sm-12 form-section-title"> Profile configuration </div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="state.method" value="editor" />
<label for="method_editor">
<div class="boxselector_header">
<pr-icon icon="'edit'" feather="true"></pr-icon>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -110,9 +110,11 @@
<span ng-if="!state.agentEndpoint">Environment URL</span>
<span ng-if="state.agentEndpoint">Environment address</span>
<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.'"
>
</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>
<div class="col-sm-9 col-lg-10">
<input

View File

@@ -138,29 +138,33 @@
</div>
</div>
<!-- !note -->
<div class="boxselector_wrapper">
<div>
<input type="radio" id="restore_file" checked="checked" />
<label for="restore_file" data-cy="init-selectLocalFile">
<div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload backup file
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="restore_file" checked="checked" />
<label for="restore_file" data-cy="init-selectLocalFile">
<div class="boxselector_header">
<pr-icon icon="'upload'" feather="true"></pr-icon>
Upload backup file
</div>
<p></p>
</label>
</div>
<p></p>
</label>
</div>
<div>
<input type="radio" id="restore_s3" disabled />
<label for="restore_s3" class="boxselector_disabled">
<div class="boxselector_header">
<pr-icon icon="'download'" feather="true"></pr-icon>
Retrieve from S3
<div>
<input type="radio" id="restore_s3" disabled />
<label for="restore_s3" class="boxselector_disabled">
<div class="boxselector_header">
<pr-icon icon="'download'" feather="true"></pr-icon>
Retrieve from S3
</div>
<p
>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>
<p
>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>
<!-- note -->

View File

@@ -21,17 +21,19 @@
</div>
<!-- !note -->
<!-- environment-type -->
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-repeat="type in ctrl.endpointSections">
<input type="radio" id="{{ type.Id }}" ng-model="ctrl.formValues.ConnectionType" ng-value="type.Value" />
<label for="{{ type.Id }}">
<div class="boxselector_header">
<i ng-class="type.Classes" aria-hidden="true" style="margin-right: 2px"></i>
{{ type.Title }}
</div>
<p>{{ type.Description }}</p>
</label>
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div ng-repeat="type in ctrl.endpointSections">
<input type="radio" id="{{ type.Id }}" ng-model="ctrl.formValues.ConnectionType" ng-value="type.Value" />
<label for="{{ type.Id }}">
<div class="boxselector_header">
<i ng-class="type.Classes" aria-hidden="true" style="margin-right: 2px"></i>
{{ type.Title }}
</div>
<p>{{ type.Description }}</p>
</label>
</div>
</div>
</div>
</div>

View File

@@ -7,11 +7,7 @@
<form class="form-horizontal">
<div class="col-sm-12 form-section-title"> Registry provider </div>
<div class="form-group"></div>
<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>
<box-selector radio-name="'availableRegistry'" value="$ctrl.state.registryValue" options="$ctrl.state.availableRegistry" on-change="($ctrl.setRegistry)"></box-selector>
<registry-form-quay
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.QUAY"

View File

@@ -1,10 +1,13 @@
import { Edit } from 'react-feather';
import Docker from '@/assets/ico/vendor/docker.svg?c';
import Ecr from '@/assets/ico/vendor/ecr.svg?c';
import Quay from '@/assets/ico/vendor/quay.svg?c';
import Proget from '@/assets/ico/vendor/proget.svg?c';
import Azure from '@/assets/ico/vendor/azure.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 = [
{
@@ -51,7 +54,7 @@ export const options = [
},
{
id: 'registry_custom',
icon: Custom,
icon: <BadgeIcon icon={Edit} />,
label: 'Custom registry',
description: 'Define your own registry',
value: '3',

View File

@@ -1,12 +1,16 @@
import { ArrowDownCircle } from 'react-feather';
import { FeatureId } from '@/portainer/feature-flags/enums';
import Microsoft from '@/assets/ico/vendor/microsoft.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 = [
{
id: 'auth_internal',
icon: 'svg-internal',
icon: <BadgeIcon icon={ArrowDownCircle} />,
label: 'Internal',
description: 'Internal authentication mechanism',
value: 1,
@@ -28,7 +32,7 @@ export const options = [
},
{
id: 'auth_oauth',
icon: Oauth,
icon: OAuth,
label: 'OAuth',
description: 'OAuth authentication',
value: 3,

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