Compare commits
14 Commits
2.39.0
...
fix/EE-664
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
effba7fecf | ||
|
|
ca77b85c65 | ||
|
|
1fd4291630 | ||
|
|
08dd7f6d2a | ||
|
|
ce4b0e759c | ||
|
|
538e7a823b | ||
|
|
956e8d3c59 | ||
|
|
1c5458f0d4 | ||
|
|
f6085ffad7 | ||
|
|
490bda2eaf | ||
|
|
d601d8eb7b | ||
|
|
b0564b9238 | ||
|
|
8922585a70 | ||
|
|
d7cf2284dc |
125
.github/workflows/ci.yaml
vendored
125
.github/workflows/ci.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- '!release/*'
|
||||
- 'release/*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'develop'
|
||||
@@ -20,8 +20,8 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
DOCKER_HUB_REPO: portainerci/portainer
|
||||
NODE_ENV: testing
|
||||
DOCKER_HUB_REPO: portainerci/portainer-ce
|
||||
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
|
||||
GO_VERSION: 1.21.6
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
@@ -30,81 +30,59 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { platform: linux, arch: amd64 }
|
||||
- { platform: linux, arch: arm64 }
|
||||
- { platform: linux, arch: amd64, version: "" }
|
||||
- { platform: linux, arch: arm64, version: "" }
|
||||
- { platform: linux, arch: arm, version: "" }
|
||||
- { platform: linux, arch: ppc64le, version: "" }
|
||||
- { platform: linux, arch: s390x, version: "" }
|
||||
- { platform: windows, arch: amd64, version: 1809 }
|
||||
- { platform: windows, arch: amd64, version: ltsc2022 }
|
||||
runs-on: arc-runner-set
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- name: '[preparation] checkout the current branch'
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: '[preparation] set up golang'
|
||||
uses: actions/setup-go@v4.0.1
|
||||
uses: actions/setup-go@v5.0.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
- name: '[preparation] cache paths'
|
||||
id: cache-dir-path
|
||||
run: |
|
||||
echo "yarn-cache-dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-build-dir=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-mod-dir=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
|
||||
- name: '[preparation] cache go'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ steps.cache-dir-path.outputs.go-build-dir }}
|
||||
${{ steps.cache-dir-path.outputs.go-mod-dir }}
|
||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-
|
||||
enableCrossOsArchive: true
|
||||
- name: '[preparation] set up node.js'
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: ''
|
||||
- name: '[preparation] cache yarn'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
${{ steps.cache-dir-path.outputs.yarn-cache-dir }}
|
||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-
|
||||
enableCrossOsArchive: true
|
||||
cache: 'yarn'
|
||||
- name: '[preparation] set up qemu'
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: '[preparation] set up docker context for buildx'
|
||||
run: docker context create builders
|
||||
- name: '[preparation] set up docker buildx'
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
endpoint: builders
|
||||
- name: '[preparation] docker login'
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: '[preparation] set the container image tag'
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||
# use the release branch name as the tag for release branches
|
||||
# for instance, release/2.19 becomes 2.19
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
# use pr${{ github.event.number }} as the tag for pull requests
|
||||
# for instance, pr123
|
||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||
else
|
||||
# replace / with - in the branch name
|
||||
# for instance, feature/1.0.0 -> feature-1.0.0
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}"
|
||||
else
|
||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}-${{ matrix.config.arch }}"
|
||||
fi
|
||||
|
||||
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}" >> $GITHUB_ENV
|
||||
|
||||
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $GITHUB_ENV
|
||||
- name: '[execution] build linux & windows portainer binaries'
|
||||
run: |
|
||||
export YARN_VERSION=$(yarn --version)
|
||||
@@ -112,6 +90,12 @@ jobs:
|
||||
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
|
||||
GIT_COMMIT_HASH_LONG=${{ github.sha }}
|
||||
export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7}
|
||||
|
||||
NODE_ENV="testing"
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||
NODE_ENV="production"
|
||||
fi
|
||||
|
||||
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
|
||||
env:
|
||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||
@@ -123,35 +107,70 @@ jobs:
|
||||
else
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
||||
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
||||
fi
|
||||
fi
|
||||
env:
|
||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||
build_manifests:
|
||||
runs-on: arc-runner-set
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
needs: [build_images]
|
||||
steps:
|
||||
- name: '[preparation] docker login'
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: '[preparation] set up docker context for buildx'
|
||||
run: docker version && docker context create builders
|
||||
- name: '[preparation] set up docker buildx'
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
endpoint: builders
|
||||
- name: '[execution] build and push manifests'
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||
# use the release branch name as the tag for release branches
|
||||
# for instance, release/2.19 becomes 2.19
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
# use pr${{ github.event.number }} as the tag for pull requests
|
||||
# for instance, pr123
|
||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||
else
|
||||
# replace / with - in the branch name
|
||||
# for instance, feature/1.0.0 -> feature-1.0.0
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||
fi
|
||||
|
||||
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
|
||||
|
||||
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
|
||||
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x"
|
||||
|
||||
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
|
||||
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
|
||||
fi
|
||||
|
||||
@@ -22,7 +22,7 @@ type webhookListOperationFilters struct {
|
||||
// @tags webhooks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param filters query webhookListOperationFilters false "Filters"
|
||||
// @param filters query string false "Filters (json-string)" example({"EndpointID":1,"ResourceID":"abc12345-abcd-2345-ab12-58005b4a0260"})
|
||||
// @success 200 {array} portainer.Webhook
|
||||
// @failure 400
|
||||
// @failure 500
|
||||
|
||||
@@ -241,7 +241,10 @@ func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) err
|
||||
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
|
||||
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
|
||||
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
||||
var ingress netv1.Ingress
|
||||
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ingress.Name = info.Name
|
||||
ingress.Namespace = info.Namespace
|
||||
@@ -278,6 +281,7 @@ func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInf
|
||||
})
|
||||
}
|
||||
|
||||
ingress.Spec.Rules = make([]netv1.IngressRule, 0)
|
||||
for rule, paths := range rules {
|
||||
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
||||
Host: rule,
|
||||
@@ -299,6 +303,6 @@ func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInf
|
||||
}
|
||||
}
|
||||
|
||||
_, err := ingressClient.Update(context.Background(), &ingress, metav1.UpdateOptions{})
|
||||
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
html {
|
||||
font-size: 16px;
|
||||
overflow-y: scroll;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html[theme='dark'],
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-fit">
|
||||
<insights-box type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
|
||||
<helm-insights-box></helm-insights-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="mb-2 small text-muted"
|
||||
>Select the Helm chart to use. Bring further Helm charts into your selection list via <a ui-sref="portainer.account">User settings - Helm repositories</a>.</div
|
||||
>Select the Helm chart to use. Bring further Helm charts into your selection list via
|
||||
<a ui-sref="portainer.account({'#': 'helm-repositories'})">User settings - Helm repositories</a>.</div
|
||||
>
|
||||
<beta-alert
|
||||
is-html="true"
|
||||
|
||||
@@ -38,6 +38,7 @@ class KubernetesConfigMapConverter {
|
||||
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||
res.CreationDate = data.metadata.creationTimestamp;
|
||||
res.Yaml = yaml ? yaml.data : '';
|
||||
res.Labels = data.metadata.labels;
|
||||
|
||||
res.Data = _.concat(
|
||||
_.map(data.data, (value, key) => {
|
||||
@@ -98,6 +99,7 @@ class KubernetesConfigMapConverter {
|
||||
res.metadata.uid = data.Id;
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.metadata.labels = data.Labels || {};
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
||||
_.forEach(data.Data, (entry) => {
|
||||
if (entry.IsBinary) {
|
||||
|
||||
@@ -21,6 +21,7 @@ class KubernetesConfigurationConverter {
|
||||
if (secret.Annotations) {
|
||||
res.ServiceAccountName = secret.Annotations['kubernetes.io/service-account.name'];
|
||||
}
|
||||
res.Labels = secret.Labels;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ class KubernetesConfigurationConverter {
|
||||
});
|
||||
res.data = res.Data;
|
||||
res.ConfigurationOwner = configMap.ConfigurationOwner;
|
||||
res.Labels = configMap.Labels;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class KubernetesSecretConverter {
|
||||
res.metadata.name = secret.Name;
|
||||
res.metadata.namespace = secret.Namespace;
|
||||
res.type = secret.Type;
|
||||
res.metadata.labels = secret.Labels || {};
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
||||
|
||||
let annotation = '';
|
||||
@@ -67,6 +68,7 @@ class KubernetesSecretConverter {
|
||||
res.Name = payload.metadata.name;
|
||||
res.Namespace = payload.metadata.namespace;
|
||||
res.Type = payload.type;
|
||||
res.Labels = payload.metadata.labels || {};
|
||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||
res.CreationDate = payload.metadata.creationTimestamp;
|
||||
res.Annotations = payload.metadata.annotations;
|
||||
|
||||
@@ -21,6 +21,7 @@ const _KubernetesConfigMap = Object.freeze({
|
||||
Yaml: '',
|
||||
ConfigurationOwner: '',
|
||||
Data: [],
|
||||
Labels: {},
|
||||
});
|
||||
|
||||
export class KubernetesConfigMap {
|
||||
|
||||
@@ -14,6 +14,7 @@ const _KubernetesConfigurationFormValues = Object.freeze({
|
||||
IsSimple: true,
|
||||
ServiceAccountName: '',
|
||||
Type: KubernetesSecretTypeOptions.OPAQUE.value,
|
||||
Labels: {},
|
||||
});
|
||||
|
||||
export class KubernetesConfigurationFormValues {
|
||||
|
||||
@@ -12,6 +12,7 @@ const _KubernetesApplicationSecret = Object.freeze({
|
||||
Data: [],
|
||||
SecretType: '',
|
||||
Annotations: [],
|
||||
Labels: {},
|
||||
});
|
||||
|
||||
export class KubernetesApplicationSecret {
|
||||
|
||||
@@ -58,6 +58,7 @@ import { deploymentTypeValidation } from '@/react/kubernetes/applications/compon
|
||||
import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/components/AppDeploymentTypeFormSection/AppDeploymentTypeFormSection';
|
||||
import { EnvironmentVariablesFormSection } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/EnvironmentVariablesFormSection';
|
||||
import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/kubeEnvVarValidationSchema';
|
||||
import { HelmInsightsBox } from '@/react/kubernetes/applications/ListView/ApplicationsDatatable/HelmInsightsBox';
|
||||
|
||||
import { applicationsModule } from './applications';
|
||||
|
||||
@@ -88,6 +89,7 @@ export const ngModule = angular
|
||||
'value',
|
||||
])
|
||||
)
|
||||
.component('helmInsightsBox', r2a(HelmInsightsBox, []))
|
||||
.component(
|
||||
'namespaceAccessUsersSelector',
|
||||
r2a(NamespaceAccessUsersSelector, [
|
||||
|
||||
@@ -82,10 +82,12 @@ class KubernetesConfigurationService {
|
||||
if (formValues.Kind === KubernetesConfigurationKinds.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
configMap.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
configMap.Labels = configuration.Labels;
|
||||
await this.KubernetesConfigMapService.update(configMap);
|
||||
} else {
|
||||
const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues);
|
||||
secret.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
secret.Labels = configuration.Labels;
|
||||
await this.KubernetesSecretService.update(secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,11 +135,11 @@
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="ctrl.updateApplicationViaWebEditor()"
|
||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
|
||||
ng-disabled="ctrl.isUpdateApplicationViaWebEditorButtonDisabled() || !kubernetesApplicationCreationForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0"
|
||||
button-spinner="ctrl.state.updateWebEditorInProgress"
|
||||
>
|
||||
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
|
||||
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update application</span>
|
||||
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -403,7 +403,7 @@
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="ctrl.updateApplicationViaWebEditor()"
|
||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
|
||||
ng-disabled="ctrl.isUpdateApplicationViaWebEditorButtonDisabled() || !kubernetesApplicationCreationForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0"
|
||||
button-spinner="ctrl.state.updateWebEditorInProgress"
|
||||
>
|
||||
|
||||
@@ -38,6 +38,7 @@ class KubernetesCreateApplicationController {
|
||||
$async,
|
||||
$state,
|
||||
$timeout,
|
||||
$window,
|
||||
Notifications,
|
||||
Authentication,
|
||||
KubernetesResourcePoolService,
|
||||
@@ -58,6 +59,7 @@ class KubernetesCreateApplicationController {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$timeout = $timeout;
|
||||
this.$window = $window;
|
||||
this.Notifications = Notifications;
|
||||
this.Authentication = Authentication;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
@@ -157,6 +159,7 @@ class KubernetesCreateApplicationController {
|
||||
this.refreshReactComponent = this.refreshReactComponent.bind(this);
|
||||
this.onChangeNamespaceName = this.onChangeNamespaceName.bind(this);
|
||||
this.canSupportSharedAccess = this.canSupportSharedAccess.bind(this);
|
||||
this.isUpdateApplicationViaWebEditorButtonDisabled = this.isUpdateApplicationViaWebEditorButtonDisabled.bind(this);
|
||||
|
||||
this.$scope.$watch(
|
||||
() => this.formValues,
|
||||
@@ -255,7 +258,7 @@ class KubernetesCreateApplicationController {
|
||||
{ stackFile: this.stackFileContent, stackName: this.formValues.StackName }
|
||||
);
|
||||
this.state.isEditorDirty = false;
|
||||
await this.$state.reload(this.$state.current);
|
||||
this.$window.location.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed redeploying application');
|
||||
} finally {
|
||||
@@ -290,7 +293,7 @@ class KubernetesCreateApplicationController {
|
||||
onAutoScaleChange(values) {
|
||||
return this.$async(async () => {
|
||||
// when enabling the auto scaler, set the default values
|
||||
if (!this.oldFormValues.AutoScaler.isUsed && values.isUsed) {
|
||||
if (!this.formValues.AutoScaler.isUsed && values.isUsed) {
|
||||
this.formValues.AutoScaler = {
|
||||
isUsed: values.isUsed,
|
||||
minReplicas: 1,
|
||||
@@ -643,6 +646,10 @@ class KubernetesCreateApplicationController {
|
||||
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
|
||||
}
|
||||
|
||||
isUpdateApplicationViaWebEditorButtonDisabled() {
|
||||
return (this.savedFormValues.StackName === this.formValues.StackName && !this.state.isEditorDirty) || this.state.updateWebEditorInProgress;
|
||||
}
|
||||
|
||||
isExternalApplication() {
|
||||
if (this.application) {
|
||||
return KubernetesApplicationHelper.isExternalApplication(this.application);
|
||||
|
||||
@@ -159,6 +159,7 @@ class KubernetesConfigMapController {
|
||||
this.formValues.Type = this.configuration.Type;
|
||||
this.formValues.Kind = this.configuration.Kind;
|
||||
this.oldDataYaml = this.formValues.DataYaml;
|
||||
this.formValues.Labels = this.configuration.Labels;
|
||||
|
||||
return this.configuration;
|
||||
} catch (err) {
|
||||
|
||||
@@ -155,6 +155,7 @@ class KubernetesSecretController {
|
||||
this.formValues.Type = this.configuration.Type;
|
||||
this.formValues.Kind = this.configuration.Kind;
|
||||
this.oldDataYaml = this.formValues.DataYaml;
|
||||
this.formValues.Labels = this.configuration.Labels;
|
||||
|
||||
return this.configuration;
|
||||
} catch (err) {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
ng-disabled="ctrl.formValues.namespace_toggle && ctrl.state.BuildMethod !== ctrl.BuildMethods.HELM"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.Namespace"
|
||||
ng-change="ctrl.onChangeNamespace()"
|
||||
ng-options="namespace.Name as namespace.Name for namespace in ctrl.namespaces"
|
||||
></select>
|
||||
<span ng-if="ctrl.formValues.namespace_toggle && ctrl.state.BuildMethod !== ctrl.BuildMethods.HELM" class="small text-muted pt-[7px]"
|
||||
@@ -90,6 +91,7 @@
|
||||
stack-name="ctrl.formValues.StackName"
|
||||
set-stack-name="(ctrl.setStackName)"
|
||||
is-admin="ctrl.currentUser.isAdmin"
|
||||
stacks="ctrl.stacks"
|
||||
></kube-stack-name>
|
||||
<!-- !namespace -->
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templat
|
||||
|
||||
class KubernetesDeployController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $window, Authentication, Notifications, KubernetesResourcePoolService, StackService, CustomTemplateService) {
|
||||
constructor($async, $state, $window, Authentication, Notifications, KubernetesResourcePoolService, StackService, CustomTemplateService, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$window = $window;
|
||||
@@ -24,6 +24,7 @@ class KubernetesDeployController {
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.StackService = StackService;
|
||||
this.CustomTemplateService = CustomTemplateService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
|
||||
|
||||
@@ -78,6 +79,8 @@ class KubernetesDeployController {
|
||||
Name: '',
|
||||
};
|
||||
|
||||
this.stacks = [];
|
||||
|
||||
this.ManifestDeployTypes = KubernetesDeployManifestTypes;
|
||||
this.BuildMethods = KubernetesDeployBuildMethods;
|
||||
|
||||
@@ -92,6 +95,15 @@ class KubernetesDeployController {
|
||||
this.onChangeDeployType = this.onChangeDeployType.bind(this);
|
||||
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
|
||||
this.setStackName = this.setStackName.bind(this);
|
||||
this.onChangeNamespace = this.onChangeNamespace.bind(this);
|
||||
}
|
||||
|
||||
onChangeNamespace() {
|
||||
return this.$async(async () => {
|
||||
const applications = await this.KubernetesApplicationService.get(this.formValues.Namespace);
|
||||
const stacks = _.map(applications, (item) => item.StackName).filter((item) => item !== '');
|
||||
this.stacks = _.uniq(stacks);
|
||||
});
|
||||
}
|
||||
|
||||
onSelectHelmChart(chart) {
|
||||
@@ -377,6 +389,7 @@ class KubernetesDeployController {
|
||||
}
|
||||
}
|
||||
|
||||
this.onChangeNamespace();
|
||||
this.state.viewReady = true;
|
||||
|
||||
this.$window.onbeforeunload = () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const settingsModule = angular
|
||||
)
|
||||
.component(
|
||||
'applicationSettingsPanel',
|
||||
r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess'])
|
||||
r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess', 'settings'])
|
||||
)
|
||||
.component(
|
||||
'sslSettingsPanel',
|
||||
@@ -38,5 +38,5 @@ export const settingsModule = angular
|
||||
)
|
||||
.component(
|
||||
'kubeSettingsPanel',
|
||||
r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), [])
|
||||
r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), ['settings'])
|
||||
).name;
|
||||
|
||||
@@ -38,16 +38,7 @@ export function react2angular<T, U extends PropNames<T>[]>(
|
||||
Component: React.ComponentType<T & JSX.IntrinsicAttributes>,
|
||||
propNames: U & ([PropNames<T>] extends [U[number]] ? unknown : PropNames<T>)
|
||||
): IComponentOptions & { name: string } {
|
||||
const bindings = Object.fromEntries(
|
||||
propNames.map((key) => {
|
||||
// use two way binding for errors, to avoid shifting the layout from errors going between undefined <-> some value when using inputs.
|
||||
// See https://portainer.atlassian.net/browse/EE-6570 for more context
|
||||
if (key === 'errors') {
|
||||
return [key, '='];
|
||||
}
|
||||
return [key, '<'];
|
||||
})
|
||||
);
|
||||
const bindings = Object.fromEntries(propNames.map((key) => [key, '<']));
|
||||
|
||||
return {
|
||||
bindings,
|
||||
|
||||
@@ -17,6 +17,8 @@ interface FormFieldProps<TValue> {
|
||||
|
||||
type WithFormFieldProps<TProps, TValue> = TProps & FormFieldProps<TValue>;
|
||||
|
||||
type ValidationResult<T> = FormikErrors<T> | undefined;
|
||||
|
||||
/**
|
||||
* This utility function is used for wrapping React components with form validation.
|
||||
* When used inside an Angular form, it sets the form to invalid if the component values are invalid.
|
||||
@@ -109,6 +111,7 @@ function createFormValidatorController<TFormModel, TData = never>(
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.runValidation = this.runValidation.bind(this);
|
||||
this.validate = this.validate.bind(this);
|
||||
}
|
||||
|
||||
async handleChange(newValues: TFormModel) {
|
||||
@@ -123,21 +126,31 @@ function createFormValidatorController<TFormModel, TData = never>(
|
||||
this.form?.$setValidity('form', true, this.form);
|
||||
|
||||
const schema = schemaBuilder(this.validationData);
|
||||
this.errors = undefined;
|
||||
const errors = await (isPrimitive
|
||||
? validateForm<{ value: TFormModel }>(
|
||||
() => object({ value: schema }),
|
||||
{ value }
|
||||
).then((r) => r?.value)
|
||||
: validateForm<TFormModel>(() => schema, value));
|
||||
this.errors = await this.validate(schema, value, isPrimitive);
|
||||
|
||||
if (errors && Object.keys(errors).length > 0) {
|
||||
this.errors = errors as FormikErrors<TFormModel> | undefined;
|
||||
if (this.errors && Object.keys(this.errors).length > 0) {
|
||||
this.form?.$setValidity('form', false, this.form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
schema: SchemaOf<TFormModel>,
|
||||
value: TFormModel,
|
||||
isPrimitive: boolean
|
||||
): Promise<ValidationResult<TFormModel>> {
|
||||
return this.$async(async () => {
|
||||
if (isPrimitive) {
|
||||
const result = await validateForm<{ value: TFormModel }>(
|
||||
() => object({ value: schema }),
|
||||
{ value }
|
||||
);
|
||||
return result?.value as ValidationResult<TFormModel>;
|
||||
}
|
||||
return validateForm<TFormModel>(() => schema, value);
|
||||
});
|
||||
}
|
||||
|
||||
async $onChanges(changes: {
|
||||
values?: { currentValue: TFormModel };
|
||||
validationData?: { currentValue: TData };
|
||||
|
||||
@@ -2,19 +2,23 @@ import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
const Context = createContext<null | boolean>(null);
|
||||
Context.displayName = 'WidgetContext';
|
||||
|
||||
export function useWidgetContext() {
|
||||
const context = useContext(Context);
|
||||
|
||||
if (context == null) {
|
||||
throw new Error('Should be inside a Widget component');
|
||||
}
|
||||
}
|
||||
|
||||
export function Widget({ children }: PropsWithChildren<unknown>) {
|
||||
export function Widget({
|
||||
children,
|
||||
id,
|
||||
}: PropsWithChildren<{
|
||||
id?: string;
|
||||
}>) {
|
||||
return (
|
||||
<Context.Provider value>
|
||||
<div className="widget">{children}</div>
|
||||
<div id={id} className="widget">
|
||||
{children}
|
||||
</div>
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface Props<D extends DefaultType> extends AutomationTestingProps {
|
||||
emptyContentLabel?: string;
|
||||
title?: string;
|
||||
titleIcon?: IconProps['icon'];
|
||||
titleId?: string;
|
||||
initialTableState?: Partial<TableState>;
|
||||
isLoading?: boolean;
|
||||
description?: ReactNode;
|
||||
@@ -78,6 +79,7 @@ export function Datatable<D extends DefaultType>({
|
||||
getRowId = defaultGetRowId,
|
||||
isRowSelectable = () => true,
|
||||
title,
|
||||
titleId,
|
||||
titleIcon,
|
||||
emptyContentLabel,
|
||||
initialTableState = {},
|
||||
@@ -172,6 +174,7 @@ export function Datatable<D extends DefaultType>({
|
||||
onSearchChange={handleSearchBarChange}
|
||||
searchValue={settings.search}
|
||||
title={title}
|
||||
titleId={titleId}
|
||||
titleIcon={titleIcon}
|
||||
description={description}
|
||||
renderTableActions={() => renderTableActions(selectedItems)}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
renderTableSettings?(): ReactNode;
|
||||
renderTableActions?(): ReactNode;
|
||||
description?: ReactNode;
|
||||
titleId?: string;
|
||||
};
|
||||
|
||||
export function DatatableHeader({
|
||||
@@ -23,6 +24,7 @@ export function DatatableHeader({
|
||||
title,
|
||||
titleIcon,
|
||||
description,
|
||||
titleId,
|
||||
}: Props) {
|
||||
if (!title) {
|
||||
return null;
|
||||
@@ -37,7 +39,12 @@ export function DatatableHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Title label={title} icon={titleIcon} description={description}>
|
||||
<Table.Title
|
||||
id={titleId}
|
||||
label={title}
|
||||
icon={titleIcon}
|
||||
description={description}
|
||||
>
|
||||
{searchBar}
|
||||
{tableActions}
|
||||
{tableTitleSettings}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface Props {
|
||||
label: string;
|
||||
description?: ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function TableTitle({
|
||||
@@ -16,10 +17,11 @@ export function TableTitle({
|
||||
children,
|
||||
description,
|
||||
className,
|
||||
id,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx('toolBar flex-col', className)}>
|
||||
<div className={clsx('toolBar flex-col', className)} id={id}>
|
||||
<div className="flex w-full items-center gap-1 p-0">
|
||||
<div className="toolBarTitle">
|
||||
{icon && (
|
||||
|
||||
28
app/react/hooks/useAdminOnlyRedirect.ts
Normal file
28
app/react/hooks/useAdminOnlyRedirect.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RawParams, useRouter } from '@uirouter/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCurrentUser } from './useUser';
|
||||
|
||||
type RedirectOptions = {
|
||||
to: string;
|
||||
params?: RawParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirects to the given route if the user is not a Portainer admin.
|
||||
* @param to The route to redirect to (default is `'portainer.home'`).
|
||||
* @param params The params to pass to the route.
|
||||
*/
|
||||
export function useAdminOnlyRedirect(
|
||||
{ to, params }: RedirectOptions = { to: 'portainer.home' }
|
||||
) {
|
||||
const router = useRouter();
|
||||
|
||||
const { isAdmin } = useCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
router.stateService.go(to, params);
|
||||
}
|
||||
}, [isAdmin, to, params, router.stateService]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
export function HelmInsightsBox() {
|
||||
return (
|
||||
<InsightsBox
|
||||
header="Helm option"
|
||||
content={
|
||||
<span>
|
||||
From 2.20 and on, the Helm menu sidebar option has moved to the{' '}
|
||||
<strong>Create from manifest screen</strong> - accessed via the button
|
||||
above.
|
||||
</span>
|
||||
}
|
||||
insightCloseId="k8s-helm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import { createStore } from '@/react/kubernetes/datatables/default-kube-datatabl
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
import { KubernetesStack } from '../../types';
|
||||
import { HelmInsightsBox } from '../ApplicationsDatatable/HelmInsightsBox';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { SubRows } from './SubRows';
|
||||
@@ -88,11 +88,7 @@ export function ApplicationsStacksDatatable({
|
||||
/>
|
||||
|
||||
<div className="w-fit">
|
||||
<InsightsBox
|
||||
type="slim"
|
||||
header="From 2.18 on, you can filter this view by namespace."
|
||||
insightCloseId="k8s-namespace-filtering"
|
||||
/>
|
||||
<HelmInsightsBox />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { formatDate } from '@/portainer/filters/filters';
|
||||
import { appOwnerLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
import { ConfigMapRowData } from '../types';
|
||||
import { configurationOwnerUsernameLabel } from '../../../constants';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -12,7 +14,8 @@ export const created = columnHelper.accessor((row) => getCreatedAtText(row), {
|
||||
|
||||
function getCreatedAtText(row: ConfigMapRowData) {
|
||||
const owner =
|
||||
row.metadata?.labels?.['io.portainer.kubernetes.configuration.owner'];
|
||||
row.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.metadata?.labels?.[appOwnerLabel];
|
||||
const date = formatDate(row.metadata?.creationTimestamp);
|
||||
return owner ? `${date} by ${owner}` : date;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
import { appOwnerLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Badge } from '@@/Badge';
|
||||
|
||||
import { ConfigMapRowData } from '../types';
|
||||
import { configurationOwnerUsernameLabel } from '../../../constants';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -16,8 +18,11 @@ export const name = columnHelper.accessor(
|
||||
const isSystemToken = name?.includes('default-token-');
|
||||
const isSystemConfigMap = isSystemToken || row.isSystem;
|
||||
|
||||
const hasConfigurationOwner =
|
||||
!!row.metadata?.labels?.['io.portainer.kubernetes.configuration.owner'];
|
||||
const hasConfigurationOwner = !!(
|
||||
row.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.metadata?.labels?.[appOwnerLabel]
|
||||
);
|
||||
|
||||
return `${name} ${isSystemConfigMap ? 'system' : ''} ${
|
||||
!isSystemToken && !hasConfigurationOwner ? 'external' : ''
|
||||
} ${!row.inUse && !isSystemConfigMap ? 'unused' : ''}`;
|
||||
@@ -35,10 +40,10 @@ function Cell({ row }: CellContext<ConfigMapRowData, string>) {
|
||||
const isSystemToken = name?.includes('default-token-');
|
||||
const isSystemConfigMap = isSystemToken || row.original.isSystem;
|
||||
|
||||
const hasConfigurationOwner =
|
||||
!!row.original.metadata?.labels?.[
|
||||
'io.portainer.kubernetes.configuration.owner'
|
||||
];
|
||||
const hasConfigurationOwner = !!(
|
||||
row.original.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.original.metadata?.labels?.[appOwnerLabel]
|
||||
);
|
||||
|
||||
return (
|
||||
<Authorized authorizations="K8sConfigMapsR" childrenUnauthorized={name}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { formatDate } from '@/portainer/filters/filters';
|
||||
import { appOwnerLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
import { SecretRowData } from '../types';
|
||||
import { configurationOwnerUsernameLabel } from '../../../constants';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -12,7 +14,8 @@ export const created = columnHelper.accessor((row) => getCreatedAtText(row), {
|
||||
|
||||
function getCreatedAtText(row: SecretRowData) {
|
||||
const owner =
|
||||
row.metadata?.labels?.['io.portainer.kubernetes.configuration.owner'];
|
||||
row.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.metadata?.labels?.[appOwnerLabel];
|
||||
const date = formatDate(row.metadata?.creationTimestamp);
|
||||
return owner ? `${date} by ${owner}` : date;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
import { appOwnerLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Badge } from '@@/Badge';
|
||||
|
||||
import { SecretRowData } from '../types';
|
||||
import { configurationOwnerUsernameLabel } from '../../../constants';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -18,8 +20,10 @@ export const name = columnHelper.accessor(
|
||||
row.metadata?.annotations?.['portainer.io/registry.id'];
|
||||
const isSystemSecret = isSystemToken || row.isSystem || isRegistrySecret;
|
||||
|
||||
const hasConfigurationOwner =
|
||||
!!row.metadata?.labels?.['io.portainer.kubernetes.configuration.owner'];
|
||||
const hasConfigurationOwner = !!(
|
||||
row.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.metadata?.labels?.[appOwnerLabel]
|
||||
);
|
||||
return `${name} ${isSystemSecret ? 'system' : ''} ${
|
||||
!isSystemToken && !hasConfigurationOwner ? 'external' : ''
|
||||
} ${!row.inUse && !isSystemSecret ? 'unused' : ''}`;
|
||||
@@ -37,10 +41,10 @@ function Cell({ row }: CellContext<SecretRowData, string>) {
|
||||
const isSystemToken = name?.includes('default-token-');
|
||||
const isSystemSecret = isSystemToken || row.original.isSystem;
|
||||
|
||||
const hasConfigurationOwner =
|
||||
!!row.original.metadata?.labels?.[
|
||||
'io.portainer.kubernetes.configuration.owner'
|
||||
];
|
||||
const hasConfigurationOwner = !!(
|
||||
row.original.metadata?.labels?.[configurationOwnerUsernameLabel] ||
|
||||
row.original.metadata?.labels?.[appOwnerLabel]
|
||||
);
|
||||
|
||||
return (
|
||||
<Authorized authorizations="K8sSecretsR" childrenUnauthorized={name}>
|
||||
|
||||
2
app/react/kubernetes/configs/constants.ts
Normal file
2
app/react/kubernetes/configs/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const configurationOwnerUsernameLabel =
|
||||
'io.portainer.kubernetes.configuration.owner';
|
||||
@@ -828,6 +828,7 @@ export function CreateIngressView() {
|
||||
Paths: preparePaths(rule.IngressName, rule.Hosts),
|
||||
TLS: prepareTLS(rule.Hosts),
|
||||
Annotations: prepareAnnotations(rule.Annotations || []),
|
||||
Labels: rule.Labels,
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Rule {
|
||||
Hosts: Host[];
|
||||
Annotations?: Annotation[];
|
||||
IngressType?: string;
|
||||
Labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ServicePorts {
|
||||
|
||||
@@ -104,6 +104,7 @@ export function prepareRuleFromIngress(
|
||||
Hosts: prepareRuleHostsFromIngress(ing) || [],
|
||||
Annotations: ing.Annotations ? getAnnotationsForEdit(ing.Annotations) : [],
|
||||
IngressType: ing.Type,
|
||||
Labels: ing.Labels,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatDate } from '@/portainer/filters/filters';
|
||||
import { appOwnerLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
@@ -13,7 +14,8 @@ export const created = columnHelper.accessor(
|
||||
cell: ({ row, getValue }) => {
|
||||
const date = formatDate(getValue());
|
||||
const owner =
|
||||
row.original.Labels?.['io.portainer.kubernetes.ingress.owner'];
|
||||
row.original.Labels?.['io.portainer.kubernetes.ingress.owner'] ||
|
||||
row.original.Labels?.[appOwnerLabel];
|
||||
|
||||
return owner ? `${date} by ${owner}` : date;
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useEffect } from 'react';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import helm from '@/assets/ico/vendor/helm.svg?c';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
@@ -40,6 +41,19 @@ export function HelmRepositoryDatatable() {
|
||||
helmReposQuery.data?.UserRepositories,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// window.location.hash will get everything after the hashbang
|
||||
// the regex will match the the content after each hash
|
||||
const timeout = setTimeout(() => {
|
||||
const regEx = /#!.*#(.*)/;
|
||||
const match = window.location.hash.match(regEx);
|
||||
if (match && match[1]) {
|
||||
document.getElementById(match[1])?.scrollIntoView();
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
getRowId={(row) => String(row.Id)}
|
||||
@@ -47,8 +61,9 @@ export function HelmRepositoryDatatable() {
|
||||
description={<HelmDatatableDescription />}
|
||||
settingsManager={tableState}
|
||||
columns={columns}
|
||||
title="Helm Repositories"
|
||||
title="Helm repositories"
|
||||
titleIcon={helm}
|
||||
titleId="helm-repositories"
|
||||
renderTableActions={(selectedRows) => (
|
||||
<HelmRepositoryDatatableActions selectedItems={selectedRows} />
|
||||
)}
|
||||
@@ -64,8 +79,11 @@ function HelmDatatableDescription() {
|
||||
<TextTip color="blue" className="mb-3">
|
||||
Adding a Helm repo here only makes it available in your own user
|
||||
account's Portainer UI. Helm charts are pulled down from these repos
|
||||
(plus the globally-set Helm repo) and shown in the Create from Manifest
|
||||
screen's Helm charts list.
|
||||
(plus the{' '}
|
||||
<Link to="portainer.settings" params={{ '#': 'kubernetes-settings' }}>
|
||||
<span>globally-set Helm repo</span>
|
||||
</Link>
|
||||
) and shown in the Create from Manifest screen's Helm charts list.
|
||||
</TextTip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
|
||||
data-cy="credentials-addButton"
|
||||
icon={Plus}
|
||||
>
|
||||
Add Helm Repository
|
||||
Add Helm repository
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
import { Wand2 } from 'lucide-react';
|
||||
|
||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
} from './environment-types';
|
||||
|
||||
export function EnvironmentTypeSelectView() {
|
||||
// move this redirect logic to the router when migrating the router to react
|
||||
useAdminOnlyRedirect();
|
||||
const [types, setTypes] = useState<EnvironmentOptionValue[]>([]);
|
||||
const { trackEvent } = useAnalytics();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
EnvironmentId,
|
||||
} from '@/react/portainer/environments/types';
|
||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
|
||||
|
||||
import { Stepper } from '@@/Stepper';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
@@ -32,6 +33,8 @@ import styles from './EnvironmentsCreationView.module.css';
|
||||
import { WizardEndpointsList } from './WizardEndpointsList';
|
||||
|
||||
export function EnvironmentCreationView() {
|
||||
// move this redirect logic to the router when migrating the router to react
|
||||
useAdminOnlyRedirect();
|
||||
const {
|
||||
params: { localEndpointId: localEndpointIdParam },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||
import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c';
|
||||
import Kube from '@/assets/ico/kube.svg?c';
|
||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
@@ -15,6 +16,8 @@ import { useConnectLocalEnvironment } from './useFetchOrCreateLocalEnvironment';
|
||||
import styles from './HomeView.module.css';
|
||||
|
||||
export function HomeView() {
|
||||
// move this redirect logic to the router when migrating the router to react
|
||||
useAdminOnlyRedirect();
|
||||
const localEnvironmentAdded = useConnectLocalEnvironment();
|
||||
const { trackEvent } = useAnalytics();
|
||||
return (
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Settings as SettingsIcon } from 'lucide-react';
|
||||
import { Field, Form, Formik, useFormikContext } from 'formik';
|
||||
|
||||
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
||||
import {
|
||||
useSettings,
|
||||
useUpdateSettingsMutation,
|
||||
} from '@/react/portainer/settings/queries';
|
||||
import { useUpdateSettingsMutation } from '@/react/portainer/settings/queries';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
@@ -24,17 +21,13 @@ import { EnableTelemetryField } from './EnableTelemetryField';
|
||||
|
||||
export function ApplicationSettingsPanel({
|
||||
onSuccess,
|
||||
settings,
|
||||
}: {
|
||||
onSuccess(settings: Settings): void;
|
||||
settings: Settings;
|
||||
}) {
|
||||
const settingsQuery = useSettings();
|
||||
const mutation = useUpdateSettingsMutation();
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = settingsQuery.data;
|
||||
const initialValues: Values = {
|
||||
edgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||
enableTelemetry: settings.EnableTelemetry,
|
||||
|
||||
@@ -17,7 +17,7 @@ export function DeploymentOptionsSection() {
|
||||
|
||||
const limitedFeature = isLimitedToBE(FeatureId.ENFORCE_DEPLOYMENT_OPTIONS);
|
||||
return (
|
||||
<FormSection title="Deployment Options">
|
||||
<FormSection title="Deployment options">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
|
||||
@@ -9,7 +9,7 @@ export function HelmSection() {
|
||||
const [{ name }, { error }] = useField<string>('helmRepositoryUrl');
|
||||
|
||||
return (
|
||||
<FormSection title="Helm Repository">
|
||||
<FormSection title="Helm repository">
|
||||
<div className="mb-2">
|
||||
<TextTip color="blue">
|
||||
You can specify the URL to your own Helm repository here. See the{' '}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { LoadingButton } from '@@/buttons';
|
||||
import { Widget } from '@@/Widget';
|
||||
|
||||
import { useSettings, useUpdateSettingsMutation } from '../../queries';
|
||||
import { useUpdateSettingsMutation } from '../../queries';
|
||||
import { Settings } from '../../types';
|
||||
|
||||
import { HelmSection } from './HelmSection';
|
||||
import { KubeConfigSection } from './KubeConfigSection';
|
||||
@@ -16,19 +17,14 @@ import { FormValues } from './types';
|
||||
import { DeploymentOptionsSection } from './DeploymentOptionsSection';
|
||||
import { validation } from './validation';
|
||||
|
||||
export function KubeSettingsPanel() {
|
||||
const settingsQuery = useSettings();
|
||||
export function KubeSettingsPanel({ settings }: { settings: Settings }) {
|
||||
const queryClient = useQueryClient();
|
||||
const environmentId = useEnvironmentId(false);
|
||||
const mutation = useUpdateSettingsMutation();
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValues: FormValues = {
|
||||
helmRepositoryUrl: settingsQuery.data.HelmRepositoryURL || '',
|
||||
kubeconfigExpiry: settingsQuery.data.KubeconfigExpiry || '0',
|
||||
helmRepositoryUrl: settings.HelmRepositoryURL || '',
|
||||
kubeconfigExpiry: settings.KubeconfigExpiry || '0',
|
||||
globalDeploymentOptions: {
|
||||
...{
|
||||
requireNoteOnApplications: false,
|
||||
@@ -39,12 +35,12 @@ export function KubeSettingsPanel() {
|
||||
perEnvOverride: false,
|
||||
hideStacksFunctionality: false,
|
||||
},
|
||||
...settingsQuery.data.GlobalDeploymentOptions,
|
||||
...settings.GlobalDeploymentOptions,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Widget id="kubernetes-settings">
|
||||
<Widget.Title icon={kubeIcon} title="Kubernetes settings" />
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
@@ -66,7 +62,7 @@ export function KubeSettingsPanel() {
|
||||
loadingText="Saving"
|
||||
className="!ml-0"
|
||||
>
|
||||
Save Kubernetes Settings
|
||||
Save Kubernetes settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ function SSLSettingsPanel() {
|
||||
loadingText={reloadingPage ? 'Reloading' : 'Saving'}
|
||||
className="!ml-0"
|
||||
>
|
||||
Save SSL Settings
|
||||
Save SSL settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import angular from 'angular';
|
||||
|
||||
import { StateManager } from '@/portainer/services/types';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useSettings } from '../queries';
|
||||
import { Settings } from '../types';
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
|
||||
@@ -16,14 +18,33 @@ import { SSLSettingsPanelWrapper } from './SSLSettingsPanel/SSLSettingsPanel';
|
||||
import { ExperimentalFeatures } from './ExperimentalFeatures';
|
||||
|
||||
export function SettingsView() {
|
||||
const settingsQuery = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsQuery.data) {
|
||||
const regEx = /#!.*#(.*)/;
|
||||
const match = window.location.hash.match(regEx);
|
||||
if (match && match[1]) {
|
||||
document.getElementById(match[1])?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [settingsQuery.data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Settings" breadcrumbs="Settings" reload />
|
||||
|
||||
<div className="mx-4 space-y-4">
|
||||
<ApplicationSettingsPanel onSuccess={handleSuccess} />
|
||||
{settingsQuery.data && (
|
||||
<>
|
||||
<ApplicationSettingsPanel
|
||||
onSuccess={handleSuccess}
|
||||
settings={settingsQuery.data}
|
||||
/>
|
||||
|
||||
<KubeSettingsPanel />
|
||||
<KubeSettingsPanel settings={settingsQuery.data} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<HelmCertPanel />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user