Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed9f310eb | ||
|
|
64d481ae2f | ||
|
|
0dba9b709d | ||
|
|
dc259f2fce | ||
|
|
a44e8b04e8 | ||
|
|
8e785e8bb4 | ||
|
|
a35e18a904 | ||
|
|
75ed19b20e | ||
|
|
65d6098613 | ||
|
|
1cbf4dceeb | ||
|
|
8127ccd0f7 | ||
|
|
fc81002938 | ||
|
|
361f782e7c | ||
|
|
a0920d619e | ||
|
|
293d390e74 | ||
|
|
5fd36ee986 | ||
|
|
7c2fcb67eb | ||
|
|
2eb4453487 | ||
|
|
535e499cc5 | ||
|
|
fee315b07e | ||
|
|
d1166b5294 |
@@ -20,6 +20,8 @@ func CreateTLSConfiguration() *tls.Config {
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -945,6 +945,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.18.1
|
||||
// @version 2.18.3
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
ComposeFile: composeFile,
|
||||
AdditionalFiles: additionalFiles,
|
||||
|
||||
@@ -25,6 +25,7 @@ type stackGitUpdatePayload struct {
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -138,6 +139,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
//update retrieved stack data based on the payload
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
stack.Env = payload.Env
|
||||
stack.UpdatedBy = user.Username
|
||||
|
||||
@@ -31,6 +31,7 @@ type kubernetesGitStackUpdatePayload struct {
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -62,6 +63,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
}
|
||||
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (payload *teamCreatePayload) Validate(r *http.Request) error {
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 409 "Team already exists"
|
||||
// @failure 500 "Server error"
|
||||
// @router /team [post]
|
||||
// @router /teams [post]
|
||||
func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload teamCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
|
||||
@@ -35,7 +35,7 @@ func (payload *teamUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Team not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /team/{id} [put]
|
||||
// @router /teams/{id} [put]
|
||||
func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -1514,7 +1514,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.18.1"
|
||||
APIVersion = "2.18.3"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
3
app/assets/ico/beta.svg
Normal file
3
app/assets/ico/beta.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 8 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 14.3223V3.49792C1 2.11836 2.11832 1 3.49792 1C4.87753 1 5.99585 2.11836 5.99585 3.49792C5.99585 4.87749 4.87753 5.99585 3.49792 5.99585L3.91425 5.99609C5.52374 5.99609 6.82849 7.30084 6.82849 8.91034C6.82849 10.5198 5.52374 11.8246 3.91425 11.8246C2.30475 11.8246 1 10.5198 1 8.91034" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
@@ -15,6 +15,7 @@ import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
|
||||
import { InsightsBox } from '@/react/components/InsightsBox';
|
||||
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.docker.react.components', [])
|
||||
@@ -65,4 +66,5 @@ export const componentsModule = angular
|
||||
'className',
|
||||
])
|
||||
)
|
||||
.component('betaAlert', r2a(BetaAlert, ['className', 'message', 'isHtml']))
|
||||
.component('gpusInsights', r2a(GpusInsights, [])).name;
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<code-editor
|
||||
identifier="image-build-editor"
|
||||
placeholder="Define or paste the content of your Dockerfile here"
|
||||
yml="false"
|
||||
docker-file="true"
|
||||
on-change="(editorUpdate)"
|
||||
></code-editor>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
<page-header title="'Helm'" breadcrumbs="['Charts']" reload="true"></page-header>
|
||||
|
||||
<information-panel title-text="Information" ng-if="!$ctrl.state.chart">
|
||||
<beta-alert
|
||||
is-html="true"
|
||||
message="'Beta feature - initial version of Helm charts functionality, for more information see this <a href=\'https://www.portainer.io/blog/portainer-now-with-helm-support\' target=\'_blank\' class=\'hyperlink\'>blog post</a>.'"
|
||||
></beta-alert>
|
||||
<span class="small text-muted">
|
||||
<p class="inline-flex flex-row items-center">
|
||||
<pr-icon icon="'info'" class="vertical-center mr-1" mode="'primary'"></pr-icon>
|
||||
This is a first version for Helm charts, for more information see this <a
|
||||
class="hyperlink"
|
||||
href="https://www.portainer.io/blog/portainer-now-with-helm-support"
|
||||
target="_blank"
|
||||
>blog post</a
|
||||
>.</p
|
||||
>
|
||||
<p ng-if="$ctrl.state.globalRepository === ''" class="inline-flex items-center">
|
||||
<pr-icon icon="'info'"></pr-icon>
|
||||
<span>The Global Helm Repository is not configured.</span>
|
||||
@@ -23,7 +18,7 @@
|
||||
<!-- helmchart-form -->
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.chart">
|
||||
<rd-widget>
|
||||
<div class="toolBarTitle vertical-center text-muted px-5 pt-5">
|
||||
<div class="toolBarTitle vertical-center px-5 pt-5 text-[16px] font-medium">
|
||||
<fallback-image src="$ctrl.state.chart.icon" fallback-icon="'svg-helm'" class-name="'h-8 w-8'" size="'lg'"></fallback-image>
|
||||
{{ $ctrl.state.chart.name }}
|
||||
</div>
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
Release
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolBarTitle text-muted small vertical-center !gap-0 px-5">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'!mr-1'" class="vertical-center"></pr-icon>
|
||||
This is a first version for Helm charts, for more information see this
|
||||
<a href="https://www.portainer.io/blog/portainer-now-with-helm-support" target="_blank" class="hyperlink">blog post</a>.
|
||||
<div class="toolBarTitle vertical-center !gap-0 px-5">
|
||||
<beta-alert
|
||||
is-html="true"
|
||||
message="'Beta feature - initial version of Helm charts functionality, for more information see this <a href=\'https://www.portainer.io/blog/portainer-now-with-helm-support\' target=\'_blank\' class=\'hyperlink\'>blog post</a>.'"
|
||||
></beta-alert>
|
||||
</div>
|
||||
<rd-widget-body>
|
||||
<table class="table">
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT"
|
||||
value="ctrl.formValues"
|
||||
on-change="(ctrl.onChangeFormValues)"
|
||||
environment-type="KUBERNETES"
|
||||
is-force-pull-visible="false"
|
||||
is-additional-files-field-visible="true"
|
||||
is-auth-explanation-visible="true"
|
||||
deploy-method="{{ ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE ? 'compose' : 'manifest' }}"
|
||||
@@ -182,7 +184,7 @@
|
||||
<form class="form-horizontal mt-3">
|
||||
<div class="form-group" ng-if="ctrl.state.activeTab === 1">
|
||||
<div class="col-sm-12">
|
||||
<code-editor identifier="kubernetes-deploy-logs" read-only="true" yml="false" value="ctrl.errorLog"></code-editor>
|
||||
<code-editor identifier="kubernetes-deploy-logs" yml="true" read-only="true" value="ctrl.errorLog"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<information-panel title-text="Information">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<pr-icon icon="'flask-conical'" mode="'warning'"></pr-icon>
|
||||
This is a beta feature.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
@@ -1,3 +0,0 @@
|
||||
angular.module('portainer.app').component('betaPanel', {
|
||||
templateUrl: './betaPanel.html',
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
id="$ctrl.identifier"
|
||||
placeholder="$ctrl.placeholder"
|
||||
yaml="$ctrl.yml"
|
||||
docker-file="$ctrl.dockerFile"
|
||||
readonly="$ctrl.readOnly"
|
||||
on-change="($ctrl.handleChange)"
|
||||
value="$ctrl.value"
|
||||
|
||||
@@ -7,6 +7,7 @@ angular.module('portainer.app').component('codeEditor', {
|
||||
identifier: '@',
|
||||
placeholder: '@',
|
||||
yml: '<',
|
||||
dockerFile: '<',
|
||||
readOnly: '<',
|
||||
onChange: '<',
|
||||
value: '<',
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Switch to simple mode to define variables line by line, or load from .env file
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<code-editor identifier="environment-variables-editor" placeholder="e.g. key=value" value="$ctrl.editorText" yml="false" on-change="($ctrl.editorUpdate)"></code-editor>
|
||||
<code-editor identifier="environment-variables-editor" placeholder="e.g. key=value" value="$ctrl.editorText" on-change="($ctrl.editorUpdate)"></code-editor>
|
||||
</div>
|
||||
<div class="col-sm-12 small text-muted" ng-if="$ctrl.showHelpMessage">
|
||||
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const gitForm: IComponentOptions = {
|
||||
<react-git-form
|
||||
value="$ctrl.value"
|
||||
on-change="$ctrl.handleChange"
|
||||
environment-type="$ctrl.environmentType"
|
||||
is-docker-standalone="$ctrl.isDockerStandalone"
|
||||
deploy-method="$ctrl.deployMethod"
|
||||
is-additional-files-field-visible="$ctrl.isAdditionalFilesFieldVisible"
|
||||
@@ -22,6 +23,7 @@ export const gitForm: IComponentOptions = {
|
||||
bindings: {
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
environmentType: '@',
|
||||
isDockerStandalone: '<',
|
||||
deployMethod: '@',
|
||||
baseWebhookUrl: '@',
|
||||
|
||||
@@ -45,6 +45,7 @@ class KubernetesRedeployAppGitFormController {
|
||||
this.onChangeRef = this.onChangeRef.bind(this);
|
||||
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
||||
this.onChangeGitAuth = this.onChangeGitAuth.bind(this);
|
||||
this.onChangeTLSSkipVerify = this.onChangeTLSSkipVerify.bind(this);
|
||||
}
|
||||
|
||||
onChangeRef(value) {
|
||||
@@ -68,6 +69,12 @@ class KubernetesRedeployAppGitFormController {
|
||||
});
|
||||
}
|
||||
|
||||
onChangeTLSSkipVerify(value) {
|
||||
return this.$async(async () => {
|
||||
this.onChange({ TLSSkipVerify: value });
|
||||
});
|
||||
}
|
||||
|
||||
async onChangeAutoUpdate(values) {
|
||||
return this.$async(async () => {
|
||||
await this.onChange({
|
||||
@@ -152,6 +159,7 @@ class KubernetesRedeployAppGitFormController {
|
||||
|
||||
$onInit() {
|
||||
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
|
||||
this.formValues.TLSSkipVerify = this.stack.GitConfig.TLSSkipVerify;
|
||||
|
||||
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
value="$ctrl.formValues.AutoUpdate"
|
||||
on-change="($ctrl.onChangeAutoUpdate)"
|
||||
environment-type="KUBERNETES"
|
||||
is-force-pull-visible="false"
|
||||
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
|
||||
webhook-id="{{ $ctrl.state.webhookId }}"
|
||||
webhooks-docs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
|
||||
@@ -43,6 +44,19 @@
|
||||
is-auth-edit="$ctrl.state.isAuthEdit"
|
||||
></git-form-auth-fieldset>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.state.showConfig">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
name="TLSSkipVerify"
|
||||
checked="$ctrl.formValues.TLSSkipVerify"
|
||||
tooltip="'Enabling this will allow skipping TLS validation for any self-signed certificate.'"
|
||||
label-class="'col-sm-3 col-lg-2'"
|
||||
label="'Skip TLS Verification'"
|
||||
on-change="($ctrl.onChangeTLSSkipVerify)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<!-- #Git buttons -->
|
||||
<button
|
||||
@@ -61,6 +75,7 @@
|
||||
</span>
|
||||
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.saveGitSettings()"
|
||||
|
||||
@@ -55,6 +55,7 @@ class StackRedeployGitFormController {
|
||||
this.onChangeEnvVar = this.onChangeEnvVar.bind(this);
|
||||
this.onChangeOption = this.onChangeOption.bind(this);
|
||||
this.onChangeGitAuth = this.onChangeGitAuth.bind(this);
|
||||
this.onChangeTLSSkipVerify = this.onChangeTLSSkipVerify.bind(this);
|
||||
}
|
||||
|
||||
buildAnalyticsProperties() {
|
||||
@@ -94,6 +95,10 @@ class StackRedeployGitFormController {
|
||||
this.onChange({ Env: value });
|
||||
}
|
||||
|
||||
onChangeTLSSkipVerify(value) {
|
||||
this.onChange({ TLSSkipVerify: value });
|
||||
}
|
||||
|
||||
onChangeOption(values) {
|
||||
this.onChange({
|
||||
Option: {
|
||||
@@ -189,6 +194,7 @@ class StackRedeployGitFormController {
|
||||
|
||||
async $onInit() {
|
||||
this.formValues.RefName = this.model.ReferenceName;
|
||||
this.formValues.TLSSkipVerify = this.model.TLSSkipVerify;
|
||||
this.formValues.Env = this.stack.Env;
|
||||
|
||||
if (this.stack.Option) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<form name="$ctrl.redeployGitForm" class="form-horizontal my-8">
|
||||
<div class="col-sm-12 form-section-title"> Redeploy from git repository </div>
|
||||
|
||||
<git-form-info-panel
|
||||
class-name="'text-muted small'"
|
||||
url="$ctrl.model.URL"
|
||||
@@ -17,6 +18,7 @@
|
||||
webhook-id="{{ $ctrl.state.webhookId }}"
|
||||
webhooks-docs="https://docs.portainer.io/user/docker/stacks/webhooks"
|
||||
></git-form-auto-update-fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
@@ -37,6 +39,7 @@
|
||||
is-url-valid="true"
|
||||
stack-id="$ctrl.gitStackId"
|
||||
></git-form-ref-field>
|
||||
|
||||
<git-form-auth-fieldset
|
||||
ng-if="$ctrl.state.showConfig"
|
||||
value="$ctrl.formValues"
|
||||
@@ -45,12 +48,26 @@
|
||||
is-auth-edit="$ctrl.state.isAuthEdit"
|
||||
></git-form-auth-fieldset>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.state.showConfig">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
name="TLSSkipVerify"
|
||||
checked="$ctrl.formValues.TLSSkipVerify"
|
||||
tooltip="'Enabling this will allow skipping TLS validation for any self-signed certificate.'"
|
||||
label-class="'col-sm-3 col-lg-2'"
|
||||
label="'Skip TLS Verification'"
|
||||
on-change="($ctrl.onChangeTLSSkipVerify)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<environment-variables-panel
|
||||
ng-model="$ctrl.formValues.Env"
|
||||
explanation="These values will be used as substitutions in the stack file"
|
||||
on-change="($ctrl.onChangeEnvVar)"
|
||||
show-help-message="true"
|
||||
></environment-variables-panel>
|
||||
|
||||
<option-panel ng-if="$ctrl.stack.Type === 1 && $ctrl.endpoint.apiVersion >= 1.27" ng-model="$ctrl.formValues.Option" on-change="($ctrl.onChangeOption)"></option-panel>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<box-selector radio-name="'theme'" value="$ctrl.state.themeColor" options="$ctrl.state.availableThemes" on-change="($ctrl.setThemeColor)"></box-selector>
|
||||
|
||||
<p class="vertical-center mt-2">
|
||||
<pr-icon icon="'alert-circle'" class-name="'icon-primary'"></pr-icon>
|
||||
<span class="small">Dark and High-contrast theme are experimental. Some UI components might not display properly.</span>
|
||||
</p>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -42,7 +42,8 @@ function getBaseUrl() {
|
||||
const port = parseInt(window.location.port, 10);
|
||||
const displayPort =
|
||||
(protocol === 'http' && port === 80) ||
|
||||
(protocol === 'https' && port === 443)
|
||||
(protocol === 'https' && port === 443) ||
|
||||
Number.isNaN(port)
|
||||
? ''
|
||||
: `:${port}`;
|
||||
return `${protocol}://${hostname}${displayPort}${baseHref()}`;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const gitFormModule = angular
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(GitForm))), [
|
||||
'value',
|
||||
'onChange',
|
||||
'environmentType',
|
||||
'isDockerStandalone',
|
||||
'deployMethod',
|
||||
'isAdditionalFilesFieldVisible',
|
||||
|
||||
@@ -189,6 +189,7 @@ export const componentsModule = angular
|
||||
'id',
|
||||
'placeholder',
|
||||
'yaml',
|
||||
'dockerFile',
|
||||
'readonly',
|
||||
'onChange',
|
||||
'value',
|
||||
|
||||
@@ -284,6 +284,7 @@ angular.module('portainer.app').factory('StackService', [
|
||||
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
||||
RepositoryUsername: gitConfig.RepositoryUsername,
|
||||
RepositoryPassword: gitConfig.RepositoryPassword,
|
||||
TLSSkipVerify: gitConfig.TLSSkipVerify,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -468,6 +469,7 @@ angular.module('portainer.app').factory('StackService', [
|
||||
RepositoryUsername: gitConfig.RepositoryUsername,
|
||||
RepositoryPassword: gitConfig.RepositoryPassword,
|
||||
Prune: gitConfig.Option.Prune,
|
||||
TLSSkipVerify: gitConfig.TLSSkipVerify,
|
||||
}
|
||||
).$promise;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ axios.interceptors.request.use(async (config) => {
|
||||
return newConfig;
|
||||
});
|
||||
|
||||
export const agentTargetHeader = 'X-PortainerAgent-Target';
|
||||
|
||||
export function agentInterceptor(config: AxiosRequestConfig) {
|
||||
if (!config.url || !config.url.includes('/docker/')) {
|
||||
return config;
|
||||
@@ -35,7 +37,7 @@ export function agentInterceptor(config: AxiosRequestConfig) {
|
||||
const newConfig = { headers: config.headers || {}, ...config };
|
||||
const target = portainerAgentTargetHeader();
|
||||
if (target) {
|
||||
newConfig.headers['X-PortainerAgent-Target'] = target;
|
||||
newConfig.headers[agentTargetHeader] = target;
|
||||
}
|
||||
|
||||
if (portainerAgentManagerOperation()) {
|
||||
|
||||
@@ -11,7 +11,7 @@ function RegistryModalService(RegistryService) {
|
||||
const defaultValue = _.get(registryModel, 'Registry.Id', 0);
|
||||
|
||||
const registryId = await selectRegistry(registries, defaultValue);
|
||||
if (!registryId) {
|
||||
if (registryId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<code-editor
|
||||
read-only="orphaned"
|
||||
identifier="stack-editor"
|
||||
placeholder="# Define or paste the content of your docker compose file here"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
yml="true"
|
||||
on-change="(editorUpdate)"
|
||||
value="stackFileContent"
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
--text-cm-string-color: var(--red-3);
|
||||
--text-cm-number-color: var(--green-1);
|
||||
--text-cm-keyword-color: var(--ui-blue-dark-9);
|
||||
--text-cm-comment-color: var(--ui-orange-6);
|
||||
--text-cm-variable-name-color: var(--ui-green-8);
|
||||
--text-codemirror-color: var(--black-color);
|
||||
--bg-codemirror-color: var(--white-color);
|
||||
--bg-codemirror-gutters-color: var(--grey-17);
|
||||
@@ -60,9 +62,50 @@
|
||||
}
|
||||
|
||||
.root :global(.cm-button) {
|
||||
@apply bg-blue-8;
|
||||
color: var(--text-codemirror-color);
|
||||
background-image: none;
|
||||
border-radius: 4px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.root :global(.cm-button[name='next']),
|
||||
.root :global(.cm-button[name='replace']) {
|
||||
@apply border-blue-8 bg-blue-8 text-white;
|
||||
@apply hover:border-blue-9 hover:bg-blue-9 hover:text-white;
|
||||
@apply th-dark:hover:border-blue-7 th-dark:hover:bg-blue-7;
|
||||
}
|
||||
|
||||
.root :global(.cm-button[name='prev']),
|
||||
.root :global(.cm-button[name='replaceAll']) {
|
||||
@apply border border-solid;
|
||||
|
||||
@apply border-blue-8 bg-blue-2 text-blue-9;
|
||||
@apply hover:bg-blue-3;
|
||||
|
||||
@apply th-dark:border-blue-7 th-dark:bg-gray-10 th-dark:text-blue-3;
|
||||
@apply th-dark:hover:bg-blue-11;
|
||||
}
|
||||
|
||||
.root :global(.cm-button[name='select']) {
|
||||
@apply border-gray-5 bg-white text-gray-9;
|
||||
@apply hover:border-gray-5 hover:bg-gray-3 hover:text-gray-10;
|
||||
|
||||
/* dark mode */
|
||||
@apply th-dark:border-gray-warm-7 th-dark:bg-gray-iron-10 th-dark:text-gray-warm-4;
|
||||
@apply th-dark:hover:border-gray-6 th-dark:hover:bg-gray-iron-9 th-dark:hover:text-gray-warm-4;
|
||||
|
||||
@apply th-highcontrast:border-gray-2 th-highcontrast:bg-black th-highcontrast:text-white;
|
||||
@apply th-highcontrast:hover:border-gray-6 th-highcontrast:hover:bg-gray-9 th-highcontrast:hover:text-gray-warm-4;
|
||||
}
|
||||
|
||||
.root :global(.cm-search) label {
|
||||
font-weight: 400;
|
||||
@apply text-gray-7;
|
||||
@apply th-dark:text-gray-warm-3;
|
||||
@apply th-highcontrast:text-white;
|
||||
}
|
||||
|
||||
.root :global(.cm-search) input {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.root :global(.cm-textfield) {
|
||||
@@ -72,5 +115,5 @@
|
||||
}
|
||||
|
||||
.root :global(.cm-content[contenteditable='true']) {
|
||||
@apply h-full;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { StreamLanguage, LanguageSupport } from '@codemirror/language';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { useMemo } from 'react';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import { tags as highlightTags } from '@lezer/highlight';
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
yaml?: boolean;
|
||||
dockerFile?: boolean;
|
||||
readonly?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
@@ -37,10 +39,18 @@ const theme = createTheme({
|
||||
},
|
||||
{ tag: highlightTags.number, color: 'var(--text-cm-number-color)' },
|
||||
{ tag: highlightTags.keyword, color: 'var(--text-cm-keyword-color)' },
|
||||
{ tag: highlightTags.comment, color: 'var(--text-cm-comment-color)' },
|
||||
{
|
||||
tag: highlightTags.variableName,
|
||||
color: 'var(--text-cm-variable-name-color)',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const yamlLanguage = new LanguageSupport(StreamLanguage.define(yaml));
|
||||
const dockerFileLanguage = new LanguageSupport(
|
||||
StreamLanguage.define(dockerFile)
|
||||
);
|
||||
|
||||
export function CodeEditor({
|
||||
id,
|
||||
@@ -50,8 +60,18 @@ export function CodeEditor({
|
||||
value,
|
||||
height = '500px',
|
||||
yaml: isYaml,
|
||||
dockerFile: isDockerFile,
|
||||
}: Props) {
|
||||
const extensions = useMemo(() => (isYaml ? [yamlLanguage] : []), [isYaml]);
|
||||
const extensions = useMemo(() => {
|
||||
const extensions = [];
|
||||
if (isYaml) {
|
||||
extensions.push(yamlLanguage);
|
||||
}
|
||||
if (isDockerFile) {
|
||||
extensions.push(dockerFileLanguage);
|
||||
}
|
||||
return extensions;
|
||||
}, [isYaml, isDockerFile]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,6 +85,10 @@ export function CodeEditor({
|
||||
id={id}
|
||||
extensions={extensions}
|
||||
height={height}
|
||||
basicSetup={{
|
||||
highlightSelectionMatches: false,
|
||||
autocompletion: false,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ import nomadicon from '@/assets/ico/vendor/nomad-icon.svg?c';
|
||||
import openldap from '@/assets/ico/vendor/openldap.svg?c';
|
||||
import proget from '@/assets/ico/vendor/proget.svg?c';
|
||||
import quay from '@/assets/ico/vendor/quay.svg?c';
|
||||
import beta from '@/assets/ico/beta.svg?c';
|
||||
|
||||
const placeholder = Placeholder;
|
||||
|
||||
@@ -76,6 +77,7 @@ export const SvgIcons = {
|
||||
proget,
|
||||
quay,
|
||||
kube,
|
||||
beta,
|
||||
};
|
||||
|
||||
interface SvgProps {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Props {
|
||||
icon?: React.ReactNode;
|
||||
color?: Color;
|
||||
className?: string;
|
||||
childrenWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export function TextTip({
|
||||
@@ -17,11 +18,13 @@ export function TextTip({
|
||||
icon = AlertCircle,
|
||||
className,
|
||||
children,
|
||||
childrenWrapperClassName = 'text-muted',
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className={clsx('small inline-flex items-center gap-1', className)}>
|
||||
<Icon icon={icon} mode={getMode(color)} className="shrink-0" />
|
||||
<span className="text-muted">{children}</span>
|
||||
<div className={clsx('small inline-flex gap-1', className)}>
|
||||
<Icon icon={icon} mode={getMode(color)} className="!mt-[2px]" />
|
||||
|
||||
<span className={childrenWrapperClassName}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,9 +64,12 @@ function OptionItem({
|
||||
color="light"
|
||||
as="label"
|
||||
disabled={disabled || readOnly}
|
||||
className={clsx({
|
||||
active: selected,
|
||||
})}
|
||||
className={clsx(
|
||||
{
|
||||
active: selected,
|
||||
},
|
||||
'!static !z-auto'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<input
|
||||
|
||||
@@ -52,12 +52,9 @@ export function ContainersDatatable({
|
||||
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const containersQuery = useContainers(
|
||||
environment.Id,
|
||||
true,
|
||||
undefined,
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
const containersQuery = useContainers(environment.Id, {
|
||||
autoRefreshRate: settings.autoRefreshRate * 1000,
|
||||
});
|
||||
|
||||
return (
|
||||
<RowProvider context={{ environment }}>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import axios, {
|
||||
agentTargetHeader,
|
||||
parseAxiosError,
|
||||
} from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import { urlBuilder } from '../containers.service';
|
||||
import { DockerContainerResponse } from '../types/response';
|
||||
@@ -10,20 +14,27 @@ import { parseViewModel } from '../utils';
|
||||
import { Filters } from './types';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
interface UseContainers {
|
||||
all?: boolean;
|
||||
filters?: Filters;
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
export function useContainers(
|
||||
environmentId: EnvironmentId,
|
||||
all = true,
|
||||
filters?: Filters,
|
||||
autoRefreshRate?: number
|
||||
{
|
||||
autoRefreshRate,
|
||||
|
||||
...params
|
||||
}: UseContainers & {
|
||||
autoRefreshRate?: number;
|
||||
} = {}
|
||||
) {
|
||||
return useQuery(
|
||||
queryKeys.filters(environmentId, all, filters),
|
||||
() => getContainers(environmentId, all, filters),
|
||||
queryKeys.filters(environmentId, params),
|
||||
() => getContainers(environmentId, params),
|
||||
{
|
||||
meta: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to retrieve containers',
|
||||
},
|
||||
...withGlobalError('Unable to retrieve containers'),
|
||||
refetchInterval() {
|
||||
return autoRefreshRate ?? false;
|
||||
},
|
||||
@@ -33,14 +44,18 @@ export function useContainers(
|
||||
|
||||
async function getContainers(
|
||||
environmentId: EnvironmentId,
|
||||
all = true,
|
||||
filters?: Filters
|
||||
{ all = true, filters, nodeName }: UseContainers = {}
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<DockerContainerResponse[]>(
|
||||
urlBuilder(environmentId, undefined, 'json'),
|
||||
{
|
||||
params: { all, filters: filters && JSON.stringify(filters) },
|
||||
headers: nodeName
|
||||
? {
|
||||
[agentTargetHeader]: nodeName,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
return data.map((c) => parseViewModel(c));
|
||||
|
||||
@@ -8,8 +8,10 @@ export const queryKeys = {
|
||||
list: (environmentId: EnvironmentId) =>
|
||||
[dockerQueryKeys.root(environmentId), 'containers'] as const,
|
||||
|
||||
filters: (environmentId: EnvironmentId, all?: boolean, filters?: Filters) =>
|
||||
[...queryKeys.list(environmentId), { all, filters }] as const,
|
||||
filters: (
|
||||
environmentId: EnvironmentId,
|
||||
params: { all?: boolean; filters?: Filters; nodeName?: string } = {}
|
||||
) => [...queryKeys.list(environmentId), params] as const,
|
||||
|
||||
container: (environmentId: EnvironmentId, id: string) =>
|
||||
[...queryKeys.list(environmentId), id] as const,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
||||
@@ -15,7 +13,7 @@ import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||
import { isSystemNetwork } from '../network.helper';
|
||||
import { DockerNetwork, NetworkContainer } from '../types';
|
||||
import { NetworkResponseContainers } from '../types';
|
||||
|
||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||
@@ -25,28 +23,18 @@ export function ItemView() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [networkContainers, setNetworkContainers] = useState<
|
||||
NetworkContainer[]
|
||||
>([]);
|
||||
const {
|
||||
params: { id: networkId, nodeName },
|
||||
} = useCurrentStateAndParams();
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const networkQuery = useNetwork(environmentId, networkId);
|
||||
const networkQuery = useNetwork(environmentId, networkId, { nodeName });
|
||||
const deleteNetworkMutation = useDeleteNetwork();
|
||||
const filters = {
|
||||
network: [networkId],
|
||||
};
|
||||
const containersQuery = useContainers(environmentId, true, filters);
|
||||
|
||||
useEffect(() => {
|
||||
if (networkQuery.data && containersQuery.data) {
|
||||
setNetworkContainers(
|
||||
filterContainersInNetwork(networkQuery.data, containersQuery.data)
|
||||
);
|
||||
}
|
||||
}, [networkQuery.data, containersQuery.data]);
|
||||
const containersQuery = useContainers(environmentId, {
|
||||
filters: {
|
||||
network: [networkId],
|
||||
},
|
||||
nodeName,
|
||||
});
|
||||
|
||||
if (!networkQuery.data) {
|
||||
return null;
|
||||
@@ -54,6 +42,10 @@ export function ItemView() {
|
||||
|
||||
const network = networkQuery.data;
|
||||
|
||||
const networkContainers = filterContainersInNetwork(
|
||||
network.Containers,
|
||||
containersQuery.data
|
||||
);
|
||||
const resourceControl = network.Portainer?.ResourceControl
|
||||
? new ResourceControlViewModel(network.Portainer.ResourceControl)
|
||||
: undefined;
|
||||
@@ -116,24 +108,20 @@ export function ItemView() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(
|
||||
network: DockerNetwork,
|
||||
containers: DockerContainer[]
|
||||
) {
|
||||
const containersInNetwork = _.compact(
|
||||
containers.map((container) => {
|
||||
const containerInNetworkResponse = network.Containers[container.Id];
|
||||
if (containerInNetworkResponse) {
|
||||
const containerInNetwork: NetworkContainer = {
|
||||
...containerInNetworkResponse,
|
||||
Id: container.Id,
|
||||
};
|
||||
return containerInNetwork;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
return containersInNetwork;
|
||||
}
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(
|
||||
networkContainers?: NetworkResponseContainers,
|
||||
containers: DockerContainer[] = []
|
||||
) {
|
||||
if (!networkContainers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return containers
|
||||
.filter((container) => networkContainers[container.Id])
|
||||
.map((container) => ({
|
||||
...networkContainers[container.Id],
|
||||
Id: container.Id,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
||||
import { TableContainer, TableTitle } from '@@/datatables';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
@@ -42,53 +42,51 @@ export function NetworkContainersTable({
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle label="Containers in network" icon={Server} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
|
||||
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
|
||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
||||
import { TableContainer, TableTitle } from '@@/datatables';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Icon } from '@@/Icon';
|
||||
@@ -32,76 +32,74 @@ export function NetworkDetailsTable({
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle label="Network details" icon={Share2} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon={Trash2}
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon={Trash2}
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Share2 } from 'lucide-react';
|
||||
|
||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
||||
import { TableContainer, TableTitle } from '@@/datatables';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
|
||||
import { NetworkOptions } from '../types';
|
||||
@@ -19,15 +19,13 @@ export function NetworkOptionsTable({ options }: Props) {
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle label="Network options" icon={Share2} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ContainerId } from '@/react/docker/containers/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import axios, {
|
||||
agentTargetHeader,
|
||||
parseAxiosError,
|
||||
} from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
import { NetworkId, DockerNetwork } from './types';
|
||||
@@ -8,11 +11,19 @@ type NetworkAction = 'connect' | 'disconnect' | 'create';
|
||||
|
||||
export async function getNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId
|
||||
networkId: NetworkId,
|
||||
{ nodeName }: { nodeName?: string } = {}
|
||||
) {
|
||||
try {
|
||||
const { data: network } = await axios.get<DockerNetwork>(
|
||||
buildUrl(environmentId, networkId)
|
||||
buildUrl(environmentId, networkId),
|
||||
nodeName
|
||||
? {
|
||||
headers: {
|
||||
[agentTargetHeader]: nodeName,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
return network;
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,10 +14,21 @@ import {
|
||||
} from './network.service';
|
||||
import { NetworkId } from './types';
|
||||
|
||||
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) {
|
||||
export function useNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId,
|
||||
{ nodeName }: { nodeName?: string } = {}
|
||||
) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'docker', 'networks', networkId],
|
||||
() => getNetwork(environmentId, networkId),
|
||||
[
|
||||
'environments',
|
||||
environmentId,
|
||||
'docker',
|
||||
'networks',
|
||||
networkId,
|
||||
{ nodeName },
|
||||
],
|
||||
() => getNetwork(environmentId, networkId, { nodeName }),
|
||||
{
|
||||
onError: (err) => {
|
||||
notifyError('Failure', err as Error, 'Unable to get network');
|
||||
|
||||
@@ -49,14 +49,12 @@ export function StackContainersDatatable({ environment, stackName }: Props) {
|
||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||
);
|
||||
|
||||
const containersQuery = useContainers(
|
||||
environment.Id,
|
||||
true,
|
||||
{
|
||||
const containersQuery = useContainers(environment.Id, {
|
||||
filters: {
|
||||
label: [`com.docker.compose.project=${stackName}`],
|
||||
},
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
autoRefreshRate: settings.autoRefreshRate * 1000,
|
||||
});
|
||||
|
||||
return (
|
||||
<RowProvider context={{ environment }}>
|
||||
|
||||
@@ -55,7 +55,10 @@ function CreateView() {
|
||||
breadcrumbs="Edge agent update and rollback"
|
||||
/>
|
||||
|
||||
<BetaAlert />
|
||||
<BetaAlert
|
||||
className="ml-[15px] mb-2"
|
||||
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
|
||||
@@ -77,7 +77,10 @@ function ItemView() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<BetaAlert />
|
||||
<BetaAlert
|
||||
className="ml-[15px] mb-2"
|
||||
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
|
||||
@@ -42,7 +42,10 @@ export function ListView() {
|
||||
breadcrumbs="Update and rollback"
|
||||
/>
|
||||
|
||||
<BetaAlert />
|
||||
<BetaAlert
|
||||
className="ml-[15px] mb-2"
|
||||
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
|
||||
/>
|
||||
|
||||
<Datatable
|
||||
dataset={listQuery.data}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { InformationPanel } from '@@/InformationPanel';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
export function BetaAlert() {
|
||||
interface Props {
|
||||
message: string;
|
||||
className?: string;
|
||||
isHtml?: boolean;
|
||||
}
|
||||
|
||||
export function BetaAlert({ message, className, isHtml }: Props) {
|
||||
return (
|
||||
<InformationPanel title="Limited Feature">
|
||||
<TextTip>
|
||||
This feature is currently in beta and is limited to standalone linux
|
||||
edge devices.
|
||||
</TextTip>
|
||||
</InformationPanel>
|
||||
<TextTip
|
||||
icon="svg-beta"
|
||||
className={className}
|
||||
childrenWrapperClassName="text-warning"
|
||||
>
|
||||
{!isHtml ? (
|
||||
message
|
||||
) : (
|
||||
// eslint-disable-next-line react/no-danger
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
)}
|
||||
</TextTip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { refFieldValidation } from './RefField/RefField';
|
||||
interface Props {
|
||||
value: GitFormModel;
|
||||
onChange: (value: Partial<GitFormModel>) => void;
|
||||
environmentType?: 'DOCKER' | 'KUBERNETES' | undefined;
|
||||
deployMethod?: 'compose' | 'nomad' | 'manifest';
|
||||
isDockerStandalone?: boolean;
|
||||
isAdditionalFilesFieldVisible?: boolean;
|
||||
@@ -36,6 +37,7 @@ interface Props {
|
||||
export function GitForm({
|
||||
value,
|
||||
onChange,
|
||||
environmentType = 'DOCKER',
|
||||
deployMethod = 'compose',
|
||||
isDockerStandalone = false,
|
||||
isAdditionalFilesFieldVisible,
|
||||
@@ -94,6 +96,7 @@ export function GitForm({
|
||||
|
||||
{value.AutoUpdate && (
|
||||
<AutoUpdateFieldset
|
||||
environmentType={environmentType}
|
||||
webhookId={webhookId}
|
||||
baseWebhookUrl={baseWebhookUrl}
|
||||
value={value.AutoUpdate}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
import { Laptop } from 'lucide-react';
|
||||
import { FlaskConical, Laptop } from 'lucide-react';
|
||||
|
||||
import { FDOConfiguration } from '@/portainer/hostmanagement/fdo/model';
|
||||
import {
|
||||
@@ -38,7 +38,7 @@ export function SettingsFDO({ settings, onSubmit }: Props) {
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<TextTip color="blue">
|
||||
<TextTip color="blue" icon={FlaskConical}>
|
||||
Since FDO is still an experimental feature that requires additional
|
||||
infrastructure, it has been temporarily hidden in the UI.
|
||||
</TextTip>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.18.1",
|
||||
"version": "2.18.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
|
||||
Reference in New Issue
Block a user