Compare commits

..

1 Commits

Author SHA1 Message Date
Matt Hook
96a626324c fix(buildscripts): make build process more closely resemble EE (#8881) 2023-05-04 13:04:16 +12:00
64 changed files with 579 additions and 1826 deletions

View File

@@ -1,44 +0,0 @@
version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
gofmt:
enabled: true
eslint:
enabled: true
channel: "eslint-5"
config:
config: .eslintrc.yml
exclude_patterns:
- assets/
- build/
- dist/
- distribution/
- node_modules
- test/
- webpack/
- gruntfile.js
- webpack.config.js
- api/
- "!app/kubernetes/**"
- .github/
- .tmp/

View File

@@ -1,16 +1,16 @@
name: Nightly Code Security Scan
on:
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
client-dependencies:
name: Client dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
@@ -24,14 +24,14 @@ jobs:
with:
json: true
- name: Upload js security scan result as artifact
- name: Upload js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
@@ -42,7 +42,7 @@ jobs:
- name: Analyse the js result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=js_result::${result}"
@@ -50,7 +50,7 @@ jobs:
name: Server dependency check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
@@ -77,8 +77,8 @@ jobs:
name: go-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
@@ -89,7 +89,7 @@ jobs:
- name: Analyse the go result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=go_result::${result}"
@@ -114,8 +114,11 @@ jobs:
with:
node-version: 18.x
- name: Install packages and build
run: yarn install && yarn build
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
@@ -134,9 +137,9 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
@@ -144,8 +147,8 @@ jobs:
name: image-security-scan-develop-result
path: image-trivy.json
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload go result html file
@@ -156,7 +159,7 @@ jobs:
- name: Analyse the trivy result
id: set-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
echo "::set-output name=image_result::${result}"
@@ -167,7 +170,7 @@ jobs:
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
@@ -182,7 +185,7 @@ jobs:
echo ${{ matrix.image.summary }}
- name: Send Slack message
if: >-
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'

View File

@@ -12,7 +12,7 @@ on:
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
jobs:
client-dependencies:
name: Client dependency check
@@ -51,8 +51,8 @@ jobs:
echo "null" > ./js-snyk-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
@@ -63,7 +63,7 @@ jobs:
- name: Analyse the js diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
echo "::set-output name=js_diff_result::${result}"
@@ -111,8 +111,8 @@ jobs:
echo "null" > ./go-snyk-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
@@ -123,7 +123,7 @@ jobs:
- name: Analyse the go diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
echo "::set-output name=go_diff_result::${result}"
@@ -149,8 +149,11 @@ jobs:
with:
node-version: 18.x
- name: Install packages and build
run: yarn install && yarn build
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
@@ -169,9 +172,9 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
@@ -191,8 +194,8 @@ jobs:
echo "null" > ./image-trivy-develop.json
fi
- name: Export scan result to html file
run: |
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload image result html file
@@ -203,7 +206,7 @@ jobs:
- name: Analyse the image diff result
id: set-diff-matrix
run: |
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
echo "::set-output name=image_diff_result::${result}"
@@ -215,17 +218,16 @@ jobs:
github.event.pull_request &&
github.event.review.body == '/scan'
strategy:
matrix:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
steps:
- name: Check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff.status == 'failure'
matrix.imagediff.status == 'failure'
run: |
echo ${{ matrix.jsdiff.status }}
echo ${{ matrix.godiff.status }}

View File

@@ -8,12 +8,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
node-version: '18'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn test:client
run: yarn jest --maxWorkers=2
# test-server:
# runs-on: ubuntu-latest
# env:

View File

@@ -0,0 +1,29 @@
name: Validate OpenAPI specs
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Download golang modules
run: cd ./api && go get -t -v -d ./...
- uses: actions/setup-node@v3
with:
node-version: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

View File

@@ -1,53 +0,0 @@
name: Validate
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node v14
uses: actions/setup-node@v2
with:
node-version: 14
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Setup Go v1.17.3
uses: actions/setup-go@v2
with:
go-version: '^1.17.3'
- name: Prebuild docs
run: yarn prebuild:docs
- name: Build OpenAPI 2.0 Spec
run: yarn build:docs
# Install dependencies globally to bypass installing all frontend deps
- name: Install swagger2openapi and swagger-cli
run: yarn global add swagger2openapi @apidevtools/swagger-cli
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
run: yarn validate:docs

122
Makefile Normal file
View File

@@ -0,0 +1,122 @@
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
# For a list of valid GOOS and GOARCH values
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
PLATFORM=$(shell go env GOOS)
ARCH=$(shell go env GOARCH)
TAG=latest
SWAG_VERSION=v1.8.11
# build target, can be one of "production", "testing", "development"
ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
.DEFAULT_GOAL := help
.PHONY: help build-storybook build-client devops download-binaries tidy clean client-deps
##@ Building
init-dist:
@mkdir -p dist
build-storybook:
yarn storybook:build
build: build-server build-client ## Build the server and client
build-client: init-dist client-deps ## Build the client
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build ## Build the Portainer image
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
devops: clean init-dist download-binaries build-client ## Build the server binary for CI
echo "Building the devops binary..."
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
##@ Dependencies
download-binaries: ## Download dependant binaries
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
tidy: ## Tidy up the go.mod file
cd api && go mod tidy
client-deps: ## Install client dependencies
yarn
##@ Cleanup
clean: ## Remove all build and download artifacts
@echo "Clearing the dist directory..."
@rm -rf dist/*
##@ Testing
test-client: ## Run client tests
yarn test
test-server: ## Run server tests
cd api && go test -v ./...
test: test-client test-server ## Run all tests
##@ Dev
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-image ## Run the server in development mode
@./dev/run_container.sh
##@ Format
format-client: ## Format client code
yarn format
format-server: ## Format server code
cd api && go fmt ./...
format: format-client format-server ## Format all code
##@ Lint
lint: lint-client lint-server ## Lint all code
lint-client: ## Lint client code
yarn lint
lint-server: ## Lint server code
cd api && go vet ./...
##@ Extension
dev-extension: build-server build-client ## Run the extension in development mode
make local -f build/docker-extension/Makefile
##@ Docs
docs-deps: ## Install docs dependencies
go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
docs-build: docs-deps ## Build docs
cd api && swag init -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly api/docs/swagger.yaml -o api/docs/openapi.yaml
yarn swagger-cli validate api/docs/openapi.yaml
docs-clean: ## Clean docs
rm -rf api/docs
docs-validate-clean: docs-validate docs-clean ## Validate and clean docs
##@ Helpers
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

View File

@@ -20,8 +20,6 @@ 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,
},
}
}

View File

@@ -945,6 +945,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.18.3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.18.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -82,7 +82,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.18.3
// @version 2.18.2
// @description.markdown api-description.md
// @termsOfService

View File

@@ -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 /teams [post]
// @router /team [post]
func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload teamCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)

View File

@@ -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 /teams/{id} [put]
// @router /team/{id} [put]
func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
teamID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -1514,7 +1514,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.18.3"
APIVersion = "2.18.2"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 493 B

View File

@@ -15,7 +15,6 @@ 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', [])
@@ -66,5 +65,4 @@ export const componentsModule = angular
'className',
])
)
.component('betaAlert', r2a(BetaAlert, ['className', 'message', 'isHtml']))
.component('gpusInsights', r2a(GpusInsights, [])).name;

View File

@@ -87,7 +87,7 @@
<code-editor
identifier="image-build-editor"
placeholder="Define or paste the content of your Dockerfile here"
docker-file="true"
yml="false"
on-change="(editorUpdate)"
></code-editor>
</div>

View File

@@ -1,11 +1,16 @@
<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&nbsp;<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>
@@ -18,7 +23,7 @@
<!-- helmchart-form -->
<div class="col-sm-12" ng-if="$ctrl.state.chart">
<rd-widget>
<div class="toolBarTitle vertical-center px-5 pt-5 text-[16px] font-medium">
<div class="toolBarTitle vertical-center text-muted px-5 pt-5">
<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>

View File

@@ -20,11 +20,10 @@
Release
</div>
</div>
<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 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&nbsp;
<a href="https://www.portainer.io/blog/portainer-now-with-helm-support" target="_blank" class="hyperlink">blog post</a>.
</div>
<rd-widget-body>
<table class="table">

View File

@@ -80,8 +80,6 @@
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' }}"
@@ -184,7 +182,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" yml="true" read-only="true" value="ctrl.errorLog"></code-editor>
<code-editor identifier="kubernetes-deploy-logs" read-only="true" yml="false" value="ctrl.errorLog"></code-editor>
</div>
</div>
</form>

View File

@@ -0,0 +1,8 @@
<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>

View File

@@ -0,0 +1,3 @@
angular.module('portainer.app').component('betaPanel', {
templateUrl: './betaPanel.html',
});

View File

@@ -2,7 +2,6 @@
id="$ctrl.identifier"
placeholder="$ctrl.placeholder"
yaml="$ctrl.yml"
docker-file="$ctrl.dockerFile"
readonly="$ctrl.readOnly"
on-change="($ctrl.handleChange)"
value="$ctrl.value"

View File

@@ -7,7 +7,6 @@ angular.module('portainer.app').component('codeEditor', {
identifier: '@',
placeholder: '@',
yml: '<',
dockerFile: '<',
readOnly: '<',
onChange: '<',
value: '<',

View File

@@ -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" on-change="($ctrl.editorUpdate)"></code-editor>
<code-editor identifier="environment-variables-editor" placeholder="e.g. key=value" value="$ctrl.editorText" yml="false" 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>

View File

@@ -8,7 +8,6 @@ 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"
@@ -23,7 +22,6 @@ export const gitForm: IComponentOptions = {
bindings: {
value: '<',
onChange: '<',
environmentType: '@',
isDockerStandalone: '<',
deployMethod: '@',
baseWebhookUrl: '@',

View File

@@ -9,7 +9,6 @@
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"

View File

@@ -4,6 +4,11 @@
<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>

View File

@@ -18,7 +18,6 @@ export const gitFormModule = angular
r2a(withUIRouter(withReactQuery(withCurrentUser(GitForm))), [
'value',
'onChange',
'environmentType',
'isDockerStandalone',
'deployMethod',
'isAdditionalFilesFieldVisible',

View File

@@ -189,7 +189,6 @@ export const componentsModule = angular
'id',
'placeholder',
'yaml',
'dockerFile',
'readonly',
'onChange',
'value',

View File

@@ -27,8 +27,6 @@ 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;
@@ -37,7 +35,7 @@ export function agentInterceptor(config: AxiosRequestConfig) {
const newConfig = { headers: config.headers || {}, ...config };
const target = portainerAgentTargetHeader();
if (target) {
newConfig.headers[agentTargetHeader] = target;
newConfig.headers['X-PortainerAgent-Target'] = target;
}
if (portainerAgentManagerOperation()) {

View File

@@ -4,8 +4,6 @@
--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);
@@ -62,50 +60,9 @@
}
.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) {

View File

@@ -1,7 +1,6 @@
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';
@@ -13,7 +12,6 @@ interface Props {
id: string;
placeholder?: string;
yaml?: boolean;
dockerFile?: boolean;
readonly?: boolean;
onChange: (value: string) => void;
value: string;
@@ -39,18 +37,10 @@ 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,
@@ -60,18 +50,8 @@ export function CodeEditor({
value,
height = '500px',
yaml: isYaml,
dockerFile: isDockerFile,
}: Props) {
const extensions = useMemo(() => {
const extensions = [];
if (isYaml) {
extensions.push(yamlLanguage);
}
if (isDockerFile) {
extensions.push(dockerFileLanguage);
}
return extensions;
}, [isYaml, isDockerFile]);
const extensions = useMemo(() => (isYaml ? [yamlLanguage] : []), [isYaml]);
return (
<>
@@ -85,10 +65,6 @@ export function CodeEditor({
id={id}
extensions={extensions}
height={height}
basicSetup={{
highlightSelectionMatches: false,
autocompletion: false,
}}
/>
</>
);

View File

@@ -36,7 +36,6 @@ 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;
@@ -77,7 +76,6 @@ export const SvgIcons = {
proget,
quay,
kube,
beta,
};
interface SvgProps {

View File

@@ -10,7 +10,6 @@ export interface Props {
icon?: React.ReactNode;
color?: Color;
className?: string;
childrenWrapperClassName?: string;
}
export function TextTip({
@@ -18,13 +17,11 @@ export function TextTip({
icon = AlertCircle,
className,
children,
childrenWrapperClassName = 'text-muted',
}: PropsWithChildren<Props>) {
return (
<div className={clsx('small inline-flex gap-1', className)}>
<Icon icon={icon} mode={getMode(color)} className="!mt-[2px]" />
<span className={childrenWrapperClassName}>{children}</span>
<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>
);
}

View File

@@ -64,12 +64,9 @@ function OptionItem({
color="light"
as="label"
disabled={disabled || readOnly}
className={clsx(
{
active: selected,
},
'!static !z-auto'
)}
className={clsx({
active: selected,
})}
>
{children}
<input

View File

@@ -52,9 +52,12 @@ export function ContainersDatatable({
const [search, setSearch] = useSearchBarState(storageKey);
const containersQuery = useContainers(environment.Id, {
autoRefreshRate: settings.autoRefreshRate * 1000,
});
const containersQuery = useContainers(
environment.Id,
true,
undefined,
settings.autoRefreshRate * 1000
);
return (
<RowProvider context={{ environment }}>

View File

@@ -1,11 +1,7 @@
import { useQuery } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, {
agentTargetHeader,
parseAxiosError,
} from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { urlBuilder } from '../containers.service';
import { DockerContainerResponse } from '../types/response';
@@ -14,27 +10,20 @@ 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,
{
autoRefreshRate,
...params
}: UseContainers & {
autoRefreshRate?: number;
} = {}
all = true,
filters?: Filters,
autoRefreshRate?: number
) {
return useQuery(
queryKeys.filters(environmentId, params),
() => getContainers(environmentId, params),
queryKeys.filters(environmentId, all, filters),
() => getContainers(environmentId, all, filters),
{
...withGlobalError('Unable to retrieve containers'),
meta: {
title: 'Failure',
message: 'Unable to retrieve containers',
},
refetchInterval() {
return autoRefreshRate ?? false;
},
@@ -44,18 +33,14 @@ export function useContainers(
async function getContainers(
environmentId: EnvironmentId,
{ all = true, filters, nodeName }: UseContainers = {}
all = true,
filters?: Filters
) {
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));

View File

@@ -8,10 +8,8 @@ export const queryKeys = {
list: (environmentId: EnvironmentId) =>
[dockerQueryKeys.root(environmentId), 'containers'] as const,
filters: (
environmentId: EnvironmentId,
params: { all?: boolean; filters?: Filters; nodeName?: string } = {}
) => [...queryKeys.list(environmentId), params] as const,
filters: (environmentId: EnvironmentId, all?: boolean, filters?: Filters) =>
[...queryKeys.list(environmentId), { all, filters }] as const,
container: (environmentId: EnvironmentId, id: string) =>
[...queryKeys.list(environmentId), id] as const,

View File

@@ -1,5 +1,7 @@
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';
@@ -13,7 +15,7 @@ import { PageHeader } from '@@/PageHeader';
import { useNetwork, useDeleteNetwork } from '../queries';
import { isSystemNetwork } from '../network.helper';
import { NetworkResponseContainers } from '../types';
import { DockerNetwork, NetworkContainer } from '../types';
import { NetworkDetailsTable } from './NetworkDetailsTable';
import { NetworkOptionsTable } from './NetworkOptionsTable';
@@ -23,18 +25,28 @@ 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, { nodeName });
const networkQuery = useNetwork(environmentId, networkId);
const deleteNetworkMutation = useDeleteNetwork();
const containersQuery = useContainers(environmentId, {
filters: {
network: [networkId],
},
nodeName,
});
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]);
if (!networkQuery.data) {
return null;
@@ -42,10 +54,6 @@ 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;
@@ -108,20 +116,24 @@ export function ItemView() {
);
}
}
}
function filterContainersInNetwork(
networkContainers?: NetworkResponseContainers,
containers: DockerContainer[] = []
) {
if (!networkContainers) {
return [];
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;
}
return containers
.filter((container) => networkContainers[container.Id])
.map((container) => ({
...networkContainers[container.Id],
Id: container.Id,
}));
}

View File

@@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Icon } from '@/react/components/Icon';
import { TableContainer, TableTitle } from '@@/datatables';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
@@ -42,51 +42,53 @@ export function NetworkContainersTable({
return (
<TableContainer>
<TableTitle label="Containers in network" icon={Server} />
<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,
});
}
<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,
}}
title={container.Name}
>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</DetailsTable>
{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>
</TableContainer>
);
}

View File

@@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { Authorized } from '@/react/hooks/useUser';
import { TableContainer, TableTitle } from '@@/datatables';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
@@ -32,74 +32,76 @@ export function NetworkDetailsTable({
return (
<TableContainer>
<TableTitle label="Network details" icon={Share2} />
<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()}
<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>
{/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
>
<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 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>
{/* 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>
</TableContainer>
);

View File

@@ -1,6 +1,6 @@
import { Share2 } from 'lucide-react';
import { TableContainer, TableTitle } from '@@/datatables';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { NetworkOptions } from '../types';
@@ -19,13 +19,15 @@ export function NetworkOptionsTable({ options }: Props) {
return (
<TableContainer>
<TableTitle label="Network options" icon={Share2} />
<DetailsTable dataCy="networkDetails-networkOptionsTable">
{networkEntries.map(([key, value]) => (
<DetailsTable.Row key={key} label={key}>
{value}
</DetailsTable.Row>
))}
</DetailsTable>
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-networkOptionsTable">
{networkEntries.map(([key, value]) => (
<DetailsTable.Row key={key} label={key}>
{value}
</DetailsTable.Row>
))}
</DetailsTable>
</Table>
</TableContainer>
);
}

View File

@@ -1,8 +1,5 @@
import { ContainerId } from '@/react/docker/containers/types';
import axios, {
agentTargetHeader,
parseAxiosError,
} from '@/portainer/services/axios';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { NetworkId, DockerNetwork } from './types';
@@ -11,19 +8,11 @@ type NetworkAction = 'connect' | 'disconnect' | 'create';
export async function getNetwork(
environmentId: EnvironmentId,
networkId: NetworkId,
{ nodeName }: { nodeName?: string } = {}
networkId: NetworkId
) {
try {
const { data: network } = await axios.get<DockerNetwork>(
buildUrl(environmentId, networkId),
nodeName
? {
headers: {
[agentTargetHeader]: nodeName,
},
}
: undefined
buildUrl(environmentId, networkId)
);
return network;
} catch (e) {

View File

@@ -14,21 +14,10 @@ import {
} from './network.service';
import { NetworkId } from './types';
export function useNetwork(
environmentId: EnvironmentId,
networkId: NetworkId,
{ nodeName }: { nodeName?: string } = {}
) {
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) {
return useQuery(
[
'environments',
environmentId,
'docker',
'networks',
networkId,
{ nodeName },
],
() => getNetwork(environmentId, networkId, { nodeName }),
['environments', environmentId, 'docker', 'networks', networkId],
() => getNetwork(environmentId, networkId),
{
onError: (err) => {
notifyError('Failure', err as Error, 'Unable to get network');

View File

@@ -49,12 +49,14 @@ export function StackContainersDatatable({ environment, stackName }: Props) {
columns.filter((col) => col.canHide).map((col) => col.id)
);
const containersQuery = useContainers(environment.Id, {
filters: {
const containersQuery = useContainers(
environment.Id,
true,
{
label: [`com.docker.compose.project=${stackName}`],
},
autoRefreshRate: settings.autoRefreshRate * 1000,
});
settings.autoRefreshRate * 1000
);
return (
<RowProvider context={{ environment }}>

View File

@@ -55,10 +55,7 @@ function CreateView() {
breadcrumbs="Edge agent update and rollback"
/>
<BetaAlert
className="ml-[15px] mb-2"
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
/>
<BetaAlert />
<div className="row">
<div className="col-sm-12">

View File

@@ -77,10 +77,7 @@ function ItemView() {
]}
/>
<BetaAlert
className="ml-[15px] mb-2"
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
/>
<BetaAlert />
<div className="row">
<div className="col-sm-12">

View File

@@ -42,10 +42,7 @@ export function ListView() {
breadcrumbs="Update and rollback"
/>
<BetaAlert
className="ml-[15px] mb-2"
message="Beta feature - currently limited to standalone Linux and Nomad edge devices."
/>
<BetaAlert />
<Datatable
dataset={listQuery.data}

View File

@@ -1,24 +1,13 @@
import { InformationPanel } from '@@/InformationPanel';
import { TextTip } from '@@/Tip/TextTip';
interface Props {
message: string;
className?: string;
isHtml?: boolean;
}
export function BetaAlert({ message, className, isHtml }: Props) {
export function BetaAlert() {
return (
<TextTip
icon="svg-beta"
className={className}
childrenWrapperClassName="text-warning"
>
{!isHtml ? (
message
) : (
// eslint-disable-next-line react/no-danger
<span dangerouslySetInnerHTML={{ __html: message }} />
)}
</TextTip>
<InformationPanel title="Limited Feature">
<TextTip>
This feature is currently in beta and is limited to standalone linux
edge devices.
</TextTip>
</InformationPanel>
);
}

View File

@@ -22,7 +22,6 @@ 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;
@@ -37,7 +36,6 @@ interface Props {
export function GitForm({
value,
onChange,
environmentType = 'DOCKER',
deployMethod = 'compose',
isDockerStandalone = false,
isAdditionalFilesFieldVisible,
@@ -96,7 +94,6 @@ export function GitForm({
{value.AutoUpdate && (
<AutoUpdateFieldset
environmentType={environmentType}
webhookId={webhookId}
baseWebhookUrl={baseWebhookUrl}
value={value.AutoUpdate}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Formik, Field, Form } from 'formik';
import { FlaskConical, Laptop } from 'lucide-react';
import { 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" icon={FlaskConical}>
<TextTip color="blue">
Since FDO is still an experimental feature that requires additional
infrastructure, it has been temporarily hidden in the UI.
</TextTip>

6
binary-version.json Normal file
View File

@@ -0,0 +1,6 @@
{
"docker": "v20.10.21",
"dockerCompose": "v2.17.2",
"helm": "v3.11.0",
"kubectl": "v1.24.1"
}

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
ARCHIVE_BUILD_FOLDER="/tmp/portainer-builds"
# parameter: "platform-architecture"
function build_and_push_images() {
docker build -t "portainer/portainer:$1-${VERSION}" -f build/linux/Dockerfile .
docker tag "portainer/portainer:$1-${VERSION}" "portainer/portainer:$1"
docker push "portainer/portainer:$1-${VERSION}"
docker push "portainer/portainer:$1"
}
# parameter: "platform-architecture"
function build_archive() {
BUILD_FOLDER="${ARCHIVE_BUILD_FOLDER}/$1"
rm -rf ${BUILD_FOLDER} && mkdir -pv ${BUILD_FOLDER}/portainer
cp -r dist/* ${BUILD_FOLDER}/portainer/
cd ${BUILD_FOLDER}
tar cvpfz "portainer-${VERSION}-$1.tar.gz" portainer
mv "portainer-${VERSION}-$1.tar.gz" ${ARCHIVE_BUILD_FOLDER}/
cd -
}
function build_all() {
mkdir -pv "${ARCHIVE_BUILD_FOLDER}"
for tag in $@; do
yarn grunt "release:`echo "$tag" | tr '-' ':'`"
name="portainer"; if [ "$(echo "$tag" | cut -c1)" = "w" ]; then name="${name}.exe"; fi
mv dist/portainer-$tag* dist/$name
if [ `echo $tag | cut -d \- -f 1` == 'linux' ]; then build_and_push_images "$tag"; fi
build_archive "$tag"
done
docker rmi $(docker images -q -f dangling=true)
}
if [[ $# -ne 1 ]] ; then
echo "Usage: $(basename $0) <VERSION>"
echo " $(basename $0) \"echo 'Custom' && <BASH COMMANDS>\""
exit 1
else
VERSION="$1"
if [ `echo "$@" | cut -c1-4` == 'echo' ]; then
bash -c "$@";
else
build_all 'linux-amd64 linux-arm linux-arm64 linux-ppc64le linux-s390x darwin-amd64 windows-amd64'
exit 0
fi
fi

View File

@@ -1,21 +0,0 @@
param (
[string]$platform,
[string]$arch
)
$ErrorActionPreference = "Stop";
$binary = "portainer.exe"
$project_path = $((Get-Location).Path)
New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null
Set-Location -Path "$project_path\api\cmd\portainer"
# copy templates
Copy-Item -Path "./mustache-templates" -Destination "./dist" -Recurse
C:\go\bin\go.exe get -t -d -v ./...
C:\go\bin\go.exe build -v
Move-Item -Path "$($binary)" -Destination "..\..\..\dist"

View File

@@ -1,8 +1,5 @@
#!/bin/sh
set -x
mkdir -p dist
#!/usr/bin/env bash
set -euo pipefail
# populate tool versions
BUILDNUMBER="N/A"
@@ -12,24 +9,38 @@ YARN_VERSION="0"
WEBPACK_VERSION="0"
GO_VERSION="0"
ldflags="-s -X 'github.com/portainer/liblicense.LicenseServerBaseURL=https://api.portainer.io' \
-X 'github.com/portainer/portainer-ee/api/build.BuildNumber=${BUILDNUMBER}' \
-X 'github.com/portainer/portainer-ee/api/build.ImageTag=${CONTAINER_IMAGE_TAG}' \
-X 'github.com/portainer/portainer-ee/api/build.NodejsVersion=${NODE_VERSION}' \
-X 'github.com/portainer/portainer-ee/api/build.YarnVersion=${YARN_VERSION}' \
-X 'github.com/portainer/portainer-ee/api/build.WebpackVersion=${WEBPACK_VERSION}' \
-X 'github.com/portainer/portainer-ee/api/build.GoVersion=${GO_VERSION}'"
echo "LDFLAGS=${ldflags}"
# create output folder
mkdir -p dist
# copy templates
cp -r "./mustache-templates" "./dist"
cd api || exit 1
cd api
# the go get adds 8 seconds
go get -t -d -v ./...
# the build takes 2 seconds
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build \
-trimpath \
--installsuffix cgo \
--ldflags "-s \
-trimpath \
--installsuffix cgo \
--ldflags "-s \
--X 'github.com/portainer/portainer/api/build.BuildNumber=${BUILDNUMBER}' \
--X 'github.com/portainer/portainer/api/build.ImageTag=${CONTAINER_IMAGE_TAG}' \
--X 'github.com/portainer/portainer/api/build.NodejsVersion=${NODE_VERSION}' \
--X 'github.com/portainer/portainer/api/build.YarnVersion=${YARN_VERSION}' \
--X 'github.com/portainer/portainer/api/build.WebpackVersion=${WEBPACK_VERSION}' \
--X 'github.com/portainer/portainer/api/build.GoVersion=${GO_VERSION}'" \
-o "../dist/portainer" \
./cmd/portainer/
--X 'github.com/portainer/portainer/api/build.GoVersion=${GO_VERSION}'" \
-o "../dist/portainer" \
./cmd/portainer/

23
build/download_binaries.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
PLATFORM=${1:-"linux"}
ARCH=${2:-"amd64"}
BINARY_VERSION_FILE="./binary-version.json"
dockerVersion=$(jq -r '.docker' < "${BINARY_VERSION_FILE}")
dockerComposeVersion=$(jq -r '.dockerCompose' < "${BINARY_VERSION_FILE}")
helmVersion=$(jq -r '.helm' < "${BINARY_VERSION_FILE}")
kubectlVersion=$(jq -r '.kubectl' < "${BINARY_VERSION_FILE}")
mkdir -p dist
echo "Downloading binaries for docker ${dockerVersion}, docker-compose ${dockerComposeVersion}, helm ${helmVersion}, kubectl ${kubectlVersion}"
./build/download_docker_binary.sh "$PLATFORM" "$ARCH" "$dockerVersion" &
./build/download_docker_compose_binary.sh "$PLATFORM" "$ARCH" "$dockerComposeVersion" &
./build/download_helm_binary.sh "$PLATFORM" "$ARCH" "$helmVersion" &
./build/download_kubectl_binary.sh "$PLATFORM" "$ARCH" "$kubectlVersion" &
wait

21
dev/run_container.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
PORTAINER_DATA=${PORTAINER_DATA:-/tmp/portainer}
PORTAINER_PROJECT=${PORTAINER_PROJECT:-$(pwd)}
PORTAINER_FLAGS=${PORTAINER_FLAGS:-}
docker rm -f portainer > /dev/null
docker run -d \
-p 8000:8000 \
-p 9000:9000 \
-p 9443:9443 \
-v "$PORTAINER_PROJECT/dist:/app" \
-v "$PORTAINER_DATA:/data" \
-v /var/run/docker.sock:/var/run/docker.sock:z \
-v /var/run/docker.sock:/var/run/alternative.sock:z \
-v /tmp:/tmp \
--name portainer \
portainer/base \
/app/portainer ${PORTAINER_FLAGS}

2
go.work.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=

View File

@@ -1,238 +0,0 @@
const os = require('os');
const loadGruntTasks = require('load-grunt-tasks');
const webpackDevConfig = require('./webpack/webpack.develop');
const webpackProdConfig = require('./webpack/webpack.production');
const webpackTestingConfig = require('./webpack/webpack.testing');
let arch = os.arch();
if (arch === 'x64') {
arch = 'amd64';
}
let platform = os.platform();
switch (platform) {
case 'windows':
case 'darwin':
break;
default:
platform = 'linux';
}
module.exports = function (grunt) {
loadGruntTasks(grunt, {
pattern: ['grunt-*', 'gruntify-*'],
});
grunt.initConfig({
root: 'dist',
distdir: 'dist/public',
binaries: {
dockerVersion: 'v20.10.21',
dockerComposePluginVersion: 'v2.17.2',
helmVersion: 'v3.11.0',
kubectlVersion: 'v1.24.1',
},
env: gruntConfig.env,
clean: gruntConfig.clean,
shell: gruntConfig.shell,
webpack: gruntConfig.webpack,
});
grunt.registerTask('lint', ['eslint']);
grunt.task.registerTask('build:server', 'build:server:<platform>:<arch>', function (p = platform, a = arch) {
grunt.task.run([`shell:build_binary:${p}:${a}`, `download_binaries:${p}:${a}`]);
});
grunt.registerTask('build:client', ['webpack:dev']);
grunt.registerTask('build', ['build:server', 'build:client']);
grunt.registerTask('start:server', ['build:server:linux', 'shell:run_container']);
grunt.registerTask('start:localserver', [`shell:build_binary:${platform}:${arch}`, 'shell:run_localserver']);
grunt.registerTask('start:client', ['shell:install_yarndeps', 'webpack:devWatch']);
grunt.registerTask('start', ['start:server', 'start:client']);
grunt.registerTask('start:toolkit', ['start:localserver', 'start:client']);
grunt.task.registerTask('release', 'release:<platform>:<arch>', function (platform = 'linux', a = arch) {
grunt.task.run(['env:prod', 'clean:all', `shell:build_binary:${platform}:${a}`, `download_binaries:${platform}:${a}`, 'webpack:prod']);
});
grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>:<env>', function (platform, a = arch, env = 'prod') {
grunt.task.run([
`env:${env}`,
'clean:all',
`shell:build_binary_azuredevops:${platform}:${a}`,
`download_binaries:${platform}:${a}`,
`webpack:${env}`,
`shell:storybook:${env}`,
]);
});
grunt.task.registerTask('download_binaries', 'download_binaries:<platform>:<arch>', function (platform = 'linux', a = arch) {
grunt.task.run([
`shell:download_docker_binary:${platform}:${a}`,
`shell:download_docker_compose_binary:${platform}:${a}`,
`shell:download_helm_binary:${platform}:${a}`,
`shell:download_kubectl_binary:${platform}:${a}`,
]);
});
};
/***/
const gruntConfig = {};
gruntConfig.env = {
dev: {
NODE_ENV: 'development',
},
prod: {
NODE_ENV: 'production',
},
testing: {
NODE_ENV: 'testing',
},
};
gruntConfig.webpack = {
dev: webpackDevConfig,
devWatch: Object.assign({ watch: true }, webpackDevConfig),
prod: webpackProdConfig,
testing: webpackTestingConfig,
};
gruntConfig.clean = {
server: ['<%= root %>/portainer'],
client: ['<%= distdir %>/*'],
all: ['<%= root %>/*'],
};
gruntConfig.shell = {
build_binary: { command: shell_build_binary },
build_binary_azuredevops: { command: shell_build_binary_azuredevops },
download_docker_binary: { command: shell_download_docker_binary },
download_helm_binary: { command: shell_download_helm_binary },
download_kubectl_binary: { command: shell_download_kubectl_binary },
download_docker_compose_binary: { command: shell_download_docker_compose_binary },
run_container: { command: shell_run_container },
run_localserver: { command: shell_run_localserver, options: { async: true } },
install_yarndeps: { command: shell_install_yarndeps },
storybook: { command: shell_storybook },
};
function shell_storybook(env) {
if (env === 'prod') {
return '';
}
return `
yarn build-storybook
`;
}
function shell_build_binary(platform, arch) {
const binfile = 'dist/portainer';
if (platform === 'linux' || platform === 'darwin') {
return `
if [ -f ${binfile} ]; then
echo "Portainer binary exists";
else
build/build_binary.sh ${platform} ${arch};
fi
`;
}
// windows
return `
powershell -Command "& {if (Get-Item -Path ${binfile}.exe -ErrorAction:SilentlyContinue) {
Write-Host "Portainer binary exists"
} else {
& ".\\build\\build_binary.ps1" -platform ${platform} -arch ${arch}
}}"
`;
}
function shell_build_binary_azuredevops(platform, arch) {
return `build/build_binary_azuredevops.sh ${platform} ${arch};`;
}
function shell_run_container() {
const portainerData = '${PORTAINER_DATA:-/tmp/portainer}';
const portainerRoot = process.env.PORTAINER_PROJECT ? process.env.PORTAINER_PROJECT : process.env.PWD;
const portainerFlags = '${PORTAINER_FLAGS:-}';
return `
docker rm -f portainer
docker run -d \
-p 8000:8000 \
-p 9000:9000 \
-p 9443:9443 \
-v ${portainerRoot}/dist:/app \
-v ${portainerData}:/data \
-v /var/run/docker.sock:/var/run/docker.sock:z \
-v /var/run/docker.sock:/var/run/alternative.sock:z \
-v /tmp:/tmp \
--name portainer \
portainer/base \
/app/portainer ${portainerFlags}
`;
}
function shell_run_localserver() {
return './dist/portainer';
}
function shell_install_yarndeps() {
return 'yarn';
}
function shell_download_docker_binary(platform, arch) {
const binaryVersion = '<%= binaries.dockerVersion %>';
return `
if [ -f dist/docker ] || [ -f dist/docker.exe ]; then
echo "docker binary exists";
else
build/download_docker_binary.sh ${platform} ${arch} ${binaryVersion};
fi
`;
}
function shell_download_docker_compose_binary(platform, arch) {
var binaryVersion = '<%= binaries.dockerComposePluginVersion %>';
return `
if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then
echo "docker compose binary exists";
else
build/download_docker_compose_binary.sh ${platform} ${arch} ${binaryVersion};
fi
`;
}
function shell_download_helm_binary(platform, arch) {
var binaryVersion = '<%= binaries.helmVersion %>';
return `
if [ -f dist/helm ] || [ -f dist/helm.exe ]; then
echo "helm binary exists";
else
build/download_helm_binary.sh ${platform} ${arch} ${binaryVersion};
fi
`;
}
function shell_download_kubectl_binary(platform, arch) {
var binaryVersion = '<%= binaries.kubectlVersion %>';
return `
if [ -f dist/kubectl ] || [ -f dist/kubectl.exe ]; then
echo "kubectl binary exists";
else
build/download_kubectl_binary.sh ${platform} ${arch} ${binaryVersion};
fi
`;
}

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.18.3",
"version": "2.18.2",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -17,52 +17,19 @@
}
],
"scripts": {
"build": "grunt clean:all && grunt build",
"build:server": "grunt clean:server && grunt build:server",
"build:client": "grunt clean:client && grunt build:client",
"prebuild:docs": "go install github.com/swaggo/swag/cmd/swag@v1.8.11",
"build:docs": "cd api && swag init -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./",
"validate:docs": "swagger2openapi --warnOnly api/docs/swagger.yaml -o api/docs/openapi.yaml && swagger-cli validate api/docs/openapi.yaml",
"clean": "grunt clean:all",
"start": "grunt start",
"start:clean": "grunt clean:all && grunt start",
"start:localserver": "grunt start:localserver",
"start:server": "grunt clean:server && grunt start:server",
"start:client": "grunt clean:client && grunt start:client",
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt start:client",
"dev:extension": "grunt build && make local -f build/docker-extension/Makefile",
"start:toolkit": "grunt start:toolkit",
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
"clean:all": "grunt clean:all",
"dev": "webpack-dev-server",
"start": "webpack -w",
"build": "webpack",
"format": "prettier --loglevel warn --write \"**/*.{js,css,html,jsx,tsx,ts,json}\"",
"lint": "yarn lint:client; yarn lint:server",
"lint:server": "cd api && golangci-lint run -E exportloopref",
"lint:client": "eslint --cache --fix './**/*.{js,jsx,ts,tsx}'",
"lint:pr": "make lint-pr",
"test": "yarn test:client; yarn test:server",
"test:server": "cd api && go test ./...",
"test:client": "jest --silent",
"lint": "eslint --cache --fix './**/*.{js,jsx,ts,tsx}'",
"test": "jest --silent",
"sb": "yarn storybook",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook -o ./dist/storybook",
"storybook:build": "build-storybook -o ./dist/storybook",
"analyze-webpack": "webpack --config ./webpack/webpack.analyze.js"
},
"scriptsComments": {
"build": "Build the entire app (backend/frontend) in development mode",
"build:server": "Build the backend",
"build:client": "Build the frontend (development mode)",
"build:docs": "builds API docs",
"clean": "Clean the entire dist folder",
"start": "Build the entire app (backend/frontend) in development mode, run backend inside a container and start a watch process locally for the frontend files",
"start:server": "Build the backend and run it inside a container",
"start:toolkit": "Build the entire app (backend/frontend) in development mode, run backend locally and start a watch process for the frontend files",
"clean:all": "Deprecated. Use the clean script instead",
"build:server:offline": "Deprecated. Use the build:server script instead",
"format": "Should be removed before pr is merged"
},
"engines": {
"node": ">= 0.8.4"
"node": ">= 14"
},
"dependencies": {
"@aws-crypto/sha256-js": "^2.0.0",
@@ -218,24 +185,12 @@
"eslint-plugin-regex": "^1.9.0",
"eslint-plugin-storybook": "^0.6.3",
"eslint-webpack-plugin": "^3.1.1",
"grunt": "^1.4.1",
"grunt-cli": "^1.4.3",
"grunt-contrib-clean": "^2.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-env": "^0.4.4",
"grunt-filerev": "^2.3.1",
"grunt-replace": "^1.0.1",
"grunt-shell-spawn": "^0.4.0",
"grunt-usemin": "^3.1.1",
"grunt-webpack": "^5.0.0",
"gruntify-eslint": "^3.1.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^5.5.0",
"husky": "4.2.5",
"jest": "^27.4.3",
"kubernetes-types": "^1.26.0",
"lint-staged": ">=10",
"load-grunt-tasks": "^3.5.2",
"lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.5.3",
"msw-storybook-addon": "^1.6.3",

View File

@@ -1,6 +0,0 @@
{
"description": "This file contains current tool versions",
"go_version": "v1.17.6",
"node_version": "12.x",
"yarn_version": "1.x"
}

View File

@@ -1 +1 @@
module.exports = require('./webpack/webpack.develop');
module.exports = require('./webpack/webpack.development');

985
yarn.lock

File diff suppressed because it is too large Load Diff