Merge remote-tracking branch 'origin/master' into new-eventstore

This commit is contained in:
adlerhurst 2020-11-18 10:36:02 +01:00
commit 147782332f
380 changed files with 11746 additions and 109998 deletions

16
.dockerignore Normal file
View File

@ -0,0 +1,16 @@
.git
.codecov
.github
build/dockerfile
site
console/node_modules
console/src/app/proto/generated
console/tmp
.releaserc.js
.typo-ci.yml
CONTRIBUTING.md
LICENSE
README.md
SECURITY.md
pkg/grpc/*/*.pb.*
pkg/grpc/*/*.swagger.json

View File

@ -17,7 +17,7 @@ updates:
prefix: chore
include: scope
- package-ecosystem: "docker"
directory: "/build/docker"
directory: "/build/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@ -1,54 +0,0 @@
#!/bin/bash
#debugger
set -x
source ./.github/scripts/variables.env
############################
function setup_git {
############################
echo "###############"
echo "set git config"
echo "###############"
git config --global user.email "$GIT_USER_MAIL"
git config --global user.name "$GIT_USER_NAME"
}
############################
function checkout_project {
############################
echo "###############"
echo "clone repository $GIT_URL"
echo "###############"
# clone opsrepo
git clone $GIT_URL $LOCAL_TMP_DIR/$GIT_OPSREPO
}
############################
function change_image_version {
############################
echo "###############"
echo "checkout master"
echo "###############"
cd $LOCAL_TMP_DIR/$GIT_OPSREPO/$GIT_OPSREPO_APPFOLDER/$GIT_OPSREPO_APPLICATION_NAME/overlay/$TARGET_ENVIRONMENT
git checkout master
git pull
echo "###############"
echo "change image version and commit"
echo "###############"
sed -i "s#image: $REGISTRY_IMAGE:.*#image: $REGISTRY_IMAGE:$CAOS_NEXT_VERSION#g" $GIT_OPSREPO_IMAGEFILE
git add $GIT_OPSREPO_IMAGEFILE
git commit --message "Github Workflow: $GITHUB_WORKFLOW"
}
############################
function upload_files {
############################
echo "###############"
echo "git push"
echo "###############"
git push --quiet --set-upstream origin
}

View File

@ -1,21 +0,0 @@
### local vars
export LOCAL_TMP_DIR="/tmp"
### git settings for cloning operations repository
export GIT_USER_MAIL="hi@caos.ch"
export GIT_USER_NAME="zitadel-pipeline"
#path of opsrepository
export GIT_URL="https://$GIT_OPSREPO_DEPLOYTOKEN@github.com/caos/zitadel-ops.git"
export GIT_OPSREPO="citadel-ops"
### application settings
export GIT_OPSREPO_APPFOLDER="k8s/workload"
export GIT_OPSREPO_APPLICATION_NAME="zitadel"
export GIT_OPSREPO_IMAGEFILE="imageversion.yaml"
export REGISTRY_IMAGE="$REGISTRY/$GITHUB_REPOSITORY/$IMAGE"
### environment settings
#export TARGET_ENVIRONMENT="dev"

39
.github/workflows/codecov.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Code Coverage
on: push
env:
REGISTRY: ghcr.io
NODE_VERSION: '12'
GO_VERSION: '1.15'
jobs:
container:
runs-on: ubuntu-18.04
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- uses: docker/build-push-action@v2
with:
context: .
file: ./build/dockerfile
platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ github.repository }}:coverage
push: false
cache-from: type=local,src=/tmp/.buildx-cache
target: go-codecov
outputs: type=local,dest=.
- uses: codecov/codecov-action@v1
with:
file: ./profile.cov
name: codecov-go

View File

@ -8,128 +8,49 @@ env:
GO_VERSION: '1.15'
jobs:
## Angular test, will be added later
angular-lint:
container:
runs-on: ubuntu-18.04
defaults:
run:
working-directory: ./console
steps:
- uses: actions/checkout@v2
- name: Install Protoc
uses: arduino/setup-protoc@master
with:
version: '3.x'
- run: wget -O protoc-gen-grpc-web https://github.com/grpc/grpc-web/releases/download/1.2.0/protoc-gen-grpc-web-1.2.0-linux-x86_64
- run: sudo mv protoc-gen-grpc-web /usr/local/bin/protoc-gen-grpc-web
- run: sudo chmod +x /usr/local/bin/protoc-gen-grpc-web
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run lint
angular-build:
runs-on: ubuntu-18.04
defaults:
run:
working-directory: ./console
steps:
- uses: actions/checkout@v2
- name: Install Protoc
uses: arduino/setup-protoc@master
with:
version: '3.x'
- run: wget -O protoc-gen-grpc-web https://github.com/grpc/grpc-web/releases/download/1.2.0/protoc-gen-grpc-web-1.2.0-linux-x86_64
- run: sudo mv protoc-gen-grpc-web /usr/local/bin/protoc-gen-grpc-web
- run: sudo chmod +x /usr/local/bin/protoc-gen-grpc-web
- uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run prodbuild
- uses: actions/upload-artifact@v1
with:
name: angular
path: console/dist/console
go-test:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2-beta
with:
go-version: ${{ env.GO_VERSION }}
- run: go test -race -v -coverprofile=profile.cov ./...
- uses: actions/upload-artifact@v1
with:
name: go-coverage
path: profile.cov
- uses: codecov/codecov-action@v1
with:
file: ./profile.cov
name: codecov-go
## go lint, will be added later
go-build:
runs-on: ubuntu-18.04
needs: [angular-build, angular-lint, go-test] ### We need the artifact from the angular build and that's why we wait here
name: Build ${{ matrix.goos }}-${{ matrix.goarch }}
strategy:
matrix:
goos: [ 'linux', 'darwin', 'windows' ]
goarch: ['amd64']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2-beta
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/download-artifact@v2
with:
name: angular
path: console/dist/console
- run: go get github.com/rakyll/statik
- run: ./build/console/generate-static.sh
- run: cat internal/ui/console/statik/statik.go
- run: ./build/login/generate-static.sh
- run: cat internal/ui/login/statik/statik.go
- run: ./build/notification/generate-static.sh
- run: cat internal/notification/statik/statik.go
- run: ./build/zitadel/generate-static.sh
- run: cat internal/statik/statik.go
- run: CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel-${{ matrix.goos }}-${{ matrix.goarch }} cmd/zitadel/main.go
- uses: actions/upload-artifact@v1
with:
name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
container-prod:
runs-on: ubuntu-18.04
needs: go-build
steps:
- name: Source checkout
uses: actions/checkout@v2
- uses: actions/download-artifact@v2
- name: Set output
id: branch
run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}
- name: Check output
run: echo ${{ steps.branch.outputs.short_ref }}
- name: Generate Short SHA Container Tag
id: vars
run: echo "::set-output name=sha_short::SHA-$(git rev-parse --short HEAD)"
- name: Cache Docker layers
uses: actions/cache@v2
with:
name: zitadel-linux-amd64
path: .artifacts
- uses: docker/build-push-action@v1
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
dockerfile: build/docker/Dockerfile
username: ${{ github.actor }}
password: ${{ secrets.CR_PAT }}
registry: ${{ env.REGISTRY }}
repository: ${{ github.repository }}
tag_with_ref: true
tag_with_sha: true
- uses: docker/build-push-action@v2
with:
context: .
file: ./build/dockerfile
platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.vars.outputs.sha_short }},${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.branch.outputs.short_ref }}
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache
release:
runs-on: ubuntu-18.04
needs: [container-prod]
needs: [container]
env:
DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.CR_PAT }}
@ -139,32 +60,32 @@ jobs:
- name: Generate Short SHA Container Tag
id: vars
run: echo "::set-output name=sha_short::SHA-$(git rev-parse --short HEAD)"
- uses: actions/download-artifact@v2
with:
path: .artifacts
- name: Display structure of downloaded files
run: ls -R
working-directory: .artifacts
- name: Docker Login
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
- name: Docker Pull short-sha
run: docker pull $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }}
- name: Semantic Release
id: semantic
uses: cycjimmy/semantic-release-action@v2
with:
dry_run: false
semantic_version: 17.0.4
extra_plugins: |
@semantic-release/exec@5.0.0
- name: Do something when a new release published
if: steps.semantic.outputs.new_release_published == 'true'
run: |
echo ${{ steps.semantic.outputs.new_release_version }}
echo ${{ steps.semantic.outputs.new_release_major_version }}
echo ${{ steps.semantic.outputs.new_release_minor_version }}
echo ${{ steps.semantic.outputs.new_release_patch_version }}
- name: Docker Tag Version
run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY:$CAOS_NEXT_VERSION
if: env.CAOS_NEXT_VERSION != ''
run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY:${{ steps.semantic.outputs.new_release_version }}
if: steps.semantic.outputs.new_release_published == 'true'
- name: Docker Tag Latest
run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY:latest
if: env.CAOS_NEXT_VERSION != ''
if: steps.semantic.outputs.new_release_published == 'true'
- name: Docker Push Version
run: docker push $REGISTRY/$GITHUB_REPOSITORY:$CAOS_NEXT_VERSION
if: env.CAOS_NEXT_VERSION != ''
run: docker push $REGISTRY/$GITHUB_REPOSITORY:${{ steps.semantic.outputs.new_release_version }}
if: steps.semantic.outputs.new_release_published == 'true'
- name: Docker Push Latest
run: docker push $REGISTRY/$GITHUB_REPOSITORY:latest
if: env.CAOS_NEXT_VERSION != ''
if: steps.semantic.outputs.new_release_published == 'true'

5
.gitignore vendored
View File

@ -41,4 +41,7 @@ cmd/zitadel/zitadel
# buildfolders and generated js
tmp/
console/src/app/proto/generated/
console/src/app/proto/generated/
pkg/grpc/*/*.pb.*
pkg/grpc/*/*.swagger.json

View File

@ -3,15 +3,6 @@ module.exports = {
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/github", {
"assets": [
{"path": ".artifacts/zitadel-darwin-amd64/zitadel-darwin-amd64", "label": "Darwin x86_64"},
{"path": ".artifacts/zitadel-linux-amd64/zitadel-linux-amd64", "label": "Linux x86_64"},
{"path": ".artifacts/zitadel-windows-amd64/zitadel-windows-amd64", "label": "Windows x86_64"}
]
}],
["@semantic-release/exec", {
"publishCmd": "echo '::set-env name=CAOS_NEXT_VERSION::${nextRelease.version}'"
}],
"@semantic-release/github"
]
};
};

View File

@ -2,6 +2,10 @@
## **Did you find a bug?**
## **Want to contribute code?**
* Check out our [Dev Build Guide](build/README.md).
## **Did you find a security flaw?**
* Please read [Security Policy](SECURITY.md).

View File

@ -60,4 +60,3 @@ See the policy [here](./SECURITY.md)
See the exact licensing terms [here](./LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

60
build/README.md Normal file
View File

@ -0,0 +1,60 @@
# Development
## Prerequisite
- Buildkit compatible docker installation
## Generate Proto Clients
### Angular
This command generates the grpc stub for angular into the folder console/src/app/proto/generated for local development
```Bash
DOCKER_BUILDKIT=1 docker build -f build/dockerfile . -t zitadel:local --target npm-copy -o console/src/app/proto/generated
```
### Go
With this command you can generate the stub for golang into the correct dir pkg/
```Bash
DOCKER_BUILDKIT=1 docker build -f build/dockerfile . -t zitadel:local --target go-copy -o pkg
```
## Run
### Run Angular
```Bash
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f build/docker-compose-dev.yml up --build angular
```
### Run Go
```Bash
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f build/docker-compose-dev.yml up --build go
```
### Fullstack including database
```Bash
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f build/docker-compose.yml up --build
```
## Debug
### Debug Go
```Bash
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f build/docker-compose-debug.yml up --build go
```
## Production Build
This can also be run locally!
```Bash
DOCKER_BUILDKIT=1 docker build -f build/dockerfile . -t zitadel:local --build-arg ENV=prod
```

View File

@ -4,38 +4,24 @@ set -eux
GEN_PATH=src/app/proto/generated
echo "Remove old files"
rm -rf $GEN_PATH
echo "Create folders"
mkdir -p $GEN_PATH
targetcurl () {
mkdir -p $1 && cd $1 && { curl -O $2; cd -; }
}
echo "Download additional protofiles"
targetcurl tmp/validate https://raw.githubusercontent.com/envoyproxy/protoc-gen-validate/v0.4.0/validate/validate.proto
targetcurl tmp/protoc-gen-swagger/options https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/v1.14.6/protoc-gen-swagger/options/annotations.proto
targetcurl tmp/protoc-gen-swagger/options https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/v1.14.6/protoc-gen-swagger/options/openapiv2.proto
echo "Generate grpc"
protoc \
-I=/usr/local/include \
-I=../pkg/grpc/message \
-I=../pkg/grpc/management/proto \
-I=../pkg/grpc/auth/proto \
-I=../pkg/grpc/admin/proto \
-I=../internal/protoc/protoc-gen-authoption \
-I=.tmp/protos/message \
-I=.tmp/protos/admin/proto \
-I=.tmp/protos/management/proto \
-I=.tmp/protos/auth/proto \
-I=node_modules/google-proto-files \
-I=tmp \
-I=.tmp/protos \
--js_out=import_style=commonjs,binary:$GEN_PATH \
--grpc-web_out=import_style=commonjs+dts,mode=grpcweb:$GEN_PATH \
../pkg/grpc/message/proto/*.proto \
../pkg/grpc/management/proto/*.proto \
../pkg/grpc/admin/proto/*.proto \
../pkg/grpc/auth/proto/*.proto
.tmp/protos/message/proto/*.proto \
.tmp/protos/admin/proto/*.proto \
.tmp/protos/auth/proto/*.proto \
.tmp/protos/management/proto/*.proto
echo "Generate annotations js file (compatibility)"

View File

@ -0,0 +1,30 @@
version: "3.8"
services:
angular:
build:
context: ..
dockerfile: build/dockerfile
target: dev-angular-build
args:
ENV: dev
command: sh -c "ng serve --host 0.0.0.0"
ports:
- 4200:4200
go:
build:
context: ..
dockerfile: build/dockerfile
target: dev-go-build
args:
ENV: dev
command: dlv --listen=:2345 --headless=true --log=true --log-output=debugger,debuglineerr,gdbwire,lldbout,rpc --accept-multiclient --api-version=2 debug cmd/zitadel/main.go
ports:
- 2345:2345
- 50000:50000
db:
image: cockroachdb/cockroach:v20.2.0
command: start-single-node --insecure
ports:
- 8080:8080
- 26257:26257

View File

@ -0,0 +1,31 @@
version: "3.8"
services:
angular:
build:
context: ..
dockerfile: build/dockerfile
target: dev-angular-build
args:
ENV: dev
command: sh -c "ng serve --host 0.0.0.0"
ports:
- 4200:4200
go:
build:
context: ..
dockerfile: build/dockerfile
target: dev-go-build
args:
ENV: dev
command: go run cmd/zitadel/main.go
ports:
- 50000:50000
db:
image: cockroachdb/cockroach:v20.2.0
command: start-single-node --insecure
ports:
- 8080:8080
- 26257:26257
volumes:
- "../cockroach-data/zitadel1:/cockroach/cockroach-data"

View File

@ -1,5 +0,0 @@
# Exclude system dirs
.dependabot
.github
.git

View File

@ -1,14 +0,0 @@
# This Stage prepares the user in the container and copies the files
FROM alpine:latest as prepare
RUN adduser -D zitadel
COPY .artifacts/zitadel-linux-amd64 /zitadel
COPY cmd/zitadel/*.yaml /
RUN chmod a+x /zitadel
# This Stage is intended as production image
FROM scratch as final
COPY --from=prepare /etc/passwd /etc/passwd
COPY --from=prepare / /
USER zitadel
HEALTHCHECK NONE
ENTRYPOINT ["/zitadel"]

129
build/dockerfile Normal file
View File

@ -0,0 +1,129 @@
#######################
## By default we build the prod enviroment
ARG ENV=prod
#######################
## This step downloads the protofiles, protoc and protoc-gen-grpc-web for later use
#######################
FROM alpine as base
RUN apk add tar curl
WORKDIR /.tmp
RUN wget -O protoc https://github.com/protocolbuffers/protobuf/releases/download/v3.13.0/protoc-3.13.0-linux-x86_64.zip \
&& unzip protoc \
&& wget -O bin/protoc-gen-grpc-web https://github.com/grpc/grpc-web/releases/download/1.2.0/protoc-gen-grpc-web-1.2.0-linux-x86_64 \
&& chmod +x bin/protoc-gen-grpc-web
RUN curl https://raw.githubusercontent.com/envoyproxy/protoc-gen-validate/v0.4.0/validate/validate.proto --create-dirs -o validate/validate.proto \
&& curl https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/v1.14.6/protoc-gen-swagger/options/annotations.proto --create-dirs -o protoc-gen-swagger/options/annotations.proto \
&& curl https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/v1.14.6/protoc-gen-swagger/options/openapiv2.proto --create-dirs -o protoc-gen-swagger/options/openapiv2.proto \
&& curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto --create-dirs -o google/api/annotations.proto \
&& curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto --create-dirs -o google/api/http.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/empty.proto --create-dirs -o google/protobuf/empty.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/timestamp.proto --create-dirs -o google/protobuf/timestamp.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/descriptor.proto --create-dirs -o google/protobuf/descriptor.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/duration.proto --create-dirs -o google/protobuf/duration.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/any.proto --create-dirs -o google/protobuf/any.proto \
&& curl https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/struct.proto --create-dirs -o google/protobuf/struct.proto
COPY pkg/grpc/admin/proto/admin.proto admin/proto/admin.proto
COPY pkg/grpc/auth/proto/auth.proto auth/proto/auth.proto
COPY pkg/grpc/management/proto/management.proto management/proto/management.proto
COPY pkg/grpc/message/proto/message.proto message/proto/message.proto
COPY internal/protoc/protoc-gen-authoption/authoption/options.proto authoption/options.proto
#######################
## With this step we prepare all node_modules, this helps caching the build
## Speed up this step by mounting your local node_modules directory
#######################
FROM node:12 as npm-base
WORKDIR console
COPY console/package.json console/package-lock.json ./
RUN npm install \
&& mkdir .tmp
COPY console .
COPY --from=base /.tmp/bin /usr/local/bin/
COPY --from=base /.tmp .tmp/protos/
COPY build/console build/console/
RUN build/console/generate-grpc.sh
FROM scratch as npm-copy
COPY --from=npm-base /console/src/app/proto/generated .
## anular dev build
FROM npm-base as dev-angular-build
RUN npm install -g @angular/cli
## anular prod build
FROM npm-base as prod-angular-build
RUN npm run prodbuild
#######################
## Go base build
## Speed up this step by mounting your local go mod pkg directory
#######################
FROM golang:1.15 as go-base
WORKDIR src/github.com/caos/zitadel/
COPY go.mod go.sum ./
RUN go mod download
COPY --from=base /.tmp .tmp/protos/
COPY --from=base /.tmp/bin /usr/local/bin/
COPY internal/protoc/protoc-base internal/protoc/protoc-base/
COPY internal/protoc/protoc-gen-authoption internal/protoc/protoc-gen-authoption/
RUN go install \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger \
github.com/golang/protobuf/protoc-gen-go \
github.com/envoyproxy/protoc-gen-validate
RUN go get -u github.com/go-bindata/go-bindata/...
RUN go-bindata ./internal/protoc/protoc-gen-authoption/templates \
&& go install ./internal/protoc/protoc-gen-authoption
COPY build/zitadel build/zitadel/
RUN build/zitadel/generate-grpc.sh
FROM scratch as go-copy
COPY --from=go-base /go/src/github.com/caos/zitadel/pkg/ .
## Go test
FROM go-base as go-test
COPY . .
RUN go test -race -v -coverprofile=profile.cov ./...
## Go test
FROM scratch as go-codecov
COPY --from=go-test /go/src/github.com/caos/zitadel/profile.cov profile.cov
## Go prod build
FROM go-test as prod-go-build
COPY --from=prod-angular-build console/dist/console console/dist/console/
RUN go get github.com/rakyll/statik \
&& ./build/console/generate-static.sh \
&& ./build/login/generate-static.sh \
&& ./build/notification/generate-static.sh \
&& ./build/zitadel/generate-static.sh
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel-linux-amd64 cmd/zitadel/main.go
## Go dev build
FROM go-base as dev-go-build
RUN go get github.com/go-delve/delve/cmd/dlv
#######################
## Final Production Image
#######################
FROM alpine:latest as artifact
RUN adduser -D zitadel
COPY cmd/zitadel/*.yaml /
COPY --from=prod-go-build /go/src/github.com/caos/zitadel/zitadel-linux-amd64 /zitadel
RUN chmod a+x zitadel
RUN ls -la /
## Scratch Image
FROM scratch as final
COPY --from=artifact /etc/passwd /etc/passwd
## TODO copy should be removed once the operator branch is merged
COPY --from=artifact / /
USER zitadel
HEALTHCHECK NONE
ENTRYPOINT ["/zitadel"]

65
build/zitadel/generate-grpc.sh Executable file
View File

@ -0,0 +1,65 @@
#! /bin/sh
set -eux
echo "Generate grpc"
protoc \
-I=.tmp/protos/message \
-I=.tmp/protos/admin/proto \
-I=.tmp/protos/management/proto \
-I=.tmp/protos/auth/proto \
-I=.tmp/protos \
-I=${GOPATH}/src \
--go_out=plugins=grpc:$GOPATH/src \
.tmp/protos/message/proto/message.proto
protoc \
-I=.tmp/protos/message \
-I=.tmp/protos/admin/proto \
-I=.tmp/protos/management/proto \
-I=.tmp/protos/auth/proto \
-I=.tmp/protos \
-I=${GOPATH}/src \
--go_out=plugins=grpc:$GOPATH/src \
--grpc-gateway_out=logtostderr=true:$GOPATH/src \
--swagger_out=logtostderr=true:. \
--authoption_out=. \
--validate_out=lang=go:${GOPATH}/src \
.tmp/protos/admin/proto/admin.proto
mv admin* $GOPATH/src/github.com/caos/zitadel/pkg/grpc/admin/
protoc \
-I=.tmp/protos/message \
-I=.tmp/protos/admin/proto \
-I=.tmp/protos/management/proto \
-I=.tmp/protos/auth/proto \
-I=.tmp/protos \
-I=${GOPATH}/src \
--go_out=plugins=grpc:$GOPATH/src \
--grpc-gateway_out=logtostderr=true,allow_delete_body=true:${GOPATH}/src \
--swagger_out=logtostderr=true,allow_delete_body=true:. \
--authoption_out=. \
--validate_out=lang=go:${GOPATH}/src \
.tmp/protos/management/proto/management.proto
mv management* $GOPATH/src/github.com/caos/zitadel/pkg/grpc/management/
protoc \
-I=.tmp/protos/message \
-I=.tmp/protos/admin/proto \
-I=.tmp/protos/management/proto \
-I=.tmp/protos/auth/proto \
-I=.tmp/protos \
-I=${GOPATH}/src \
--go_out=plugins=grpc:$GOPATH/src \
--grpc-gateway_out=logtostderr=true:$GOPATH/src \
--swagger_out=logtostderr=true:. \
--authoption_out=. \
--validate_out=lang=go:${GOPATH}/src \
.tmp/protos/auth/proto/auth.proto
mv auth* $GOPATH/src/github.com/caos/zitadel/pkg/grpc/auth/
echo "done generating"

View File

@ -54,8 +54,8 @@ SystemDefaults:
PasswordCheck: 240h #10d
ExternalLoginCheck: 240h #10d
MfaInitSkip: 720h #30d
MfaSoftwareCheck: 18h
MfaHardwareCheck: 12h
SecondFactorCheck: 18h
MultiFactorCheck: 12h
IamID: 'IAM'
DomainVerification:
VerificationKey:

View File

@ -50,7 +50,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,

3265
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,29 +6,29 @@
"start": "ng serve",
"build": "ng build",
"prodbuild": "ng build --prod",
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss",
"postinstall": "../build/console/generate-grpc.sh"
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.0.11",
"@angular/cdk": "~10.1.3",
"@angular/common": "~10.0.11",
"@angular/compiler": "~10.0.11",
"@angular/core": "~10.0.11",
"@angular/forms": "~10.0.11",
"@angular/material": "^10.1.3",
"@angular/platform-browser": "~10.0.11",
"@angular/platform-browser-dynamic": "~10.0.11",
"@angular/router": "~10.0.11",
"@angular/service-worker": "~10.0.11",
"@angular/animations": "~11.0.0",
"@angular/cdk": "~11.0.0",
"@angular/common": "~11.0.0",
"@angular/compiler": "~11.0.0",
"@angular/core": "~11.0.0",
"@angular/forms": "~11.0.0",
"@angular/material": "^11.0.0",
"@angular/material-moment-adapter": "^11.0.0",
"@angular/platform-browser": "~11.0.0",
"@angular/platform-browser-dynamic": "~11.0.0",
"@angular/router": "~11.0.0",
"@angular/service-worker": "~11.0.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@types/file-saver": "^2.0.1",
"@types/google-protobuf": "^3.7.4",
"@types/uuid": "^8.3.0",
"@types/google-protobuf": "^3.7.3",
"angularx-qrcode": "^10.0.11",
"angular-oauth2-oidc": "^10.0.3",
"angularx-qrcode": "^10.0.11",
"cors": "^2.8.5",
"file-saver": "^2.0.2",
"google-proto-files": "^2.2.0",
@ -36,29 +36,28 @@
"grpc": "^1.24.3",
"grpc-web": "^1.2.1",
"moment": "^2.29.1",
"ngx-moment": "^5.0.0",
"ngx-quicklink": "^0.2.4",
"ngx-quicklink": "^0.2.6",
"rxjs": "~6.6.3",
"ts-protoc-gen": "^0.13.0",
"tslib": "^2.0.3",
"tslib": "^2.0.0",
"uuid": "^8.3.1",
"zone.js": "~0.11.2"
"zone.js": "~0.11.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.0.11",
"@angular-devkit/build-angular": "~0.1100.1",
"@angular/cli": "~11.0.1",
"@angular/compiler-cli": "~11.0.0",
"@angular/language-service": "~11.0.0",
"@types/jasmine": "~3.6.0",
"@angular/language-service": "~10.2.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^14.14.3",
"codelyzer": "^6.0.1",
"@types/node": "^14.14.6",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~6.0.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.2.3",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.1",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"prettier": "^2.1.2",
"protractor": "~7.0.0",
@ -67,6 +66,6 @@
"stylelint-scss": "^3.18.0",
"ts-node": "~9.0.0",
"tslint": "~6.1.3",
"typescript": "^3.9.7"
"typescript": "^4.0.5"
}
}

View File

@ -131,6 +131,7 @@ const routes: Routes = [
routes,
{
preloadingStrategy: QuicklinkStrategy,
relativeLinkResolution: 'legacy',
},
),
],

View File

@ -29,10 +29,12 @@
placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</mat-form-field>
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
<div class="org-wrapper">
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
</div>
<button class="show-all" mat-menu-item
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
@ -70,8 +72,10 @@
</ng-container>
<ng-container *ngIf="iamuser$ | async">
<div class="divider">
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.ADMINSECTION' | translate}}</span>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/iam']">
<i class="icon las la-gem"></i>
@ -92,7 +96,7 @@
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.PROJECTSSECTION' | translate}}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']"
@ -102,17 +106,19 @@
<div class="c_label">
<span>{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}
{{'MENU.PROJECT' | translate}} </span>
<span *ngIf="ownedProjectsCount as ownedPCount"
class="count">{{ownedPCount}}</span>
<span *ngIf="(mgmtService?.ownedProjectsCount | async)"
class="count">{{mgmtService?.ownedProjectsCount | async}}</span>
</div>
</a>
<a @navitem *ngIf="grantedProjectsCount as grantPCount" class="nav-item"
[routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
<a @navitem
*ngIf="mgmtService?.grantedProjectsCount && (mgmtService?.grantedProjectsCount | async)"
class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/granted-projects']">
<i class="icon las la-layer-group"></i>
<div class="c_label">
<span>{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
<span class="count">{{grantPCount}}</span>
<span class="count">{{mgmtService?.grantedProjectsCount | async}}</span>
</div>
</a>
</ng-template>
@ -122,7 +128,7 @@
<div class="line"></div>
<span class="label">
{{ 'MENU.USERSECTION' | translate }}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']"
@ -144,7 +150,7 @@
<div class="line"></div>
<span class="label">
{{ 'MENU.GRANTSECTION' | translate }}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
@ -161,13 +167,18 @@
</div>
</mat-drawer>
<mat-drawer-content class="content">
<div @toolbar *ngIf="iamuser$ | async" class="admin-line" matTooltip="IAM Administrator">
<span>{{'MENU.IAMADMIN' | translate}}</span>
</div>
<div class="router" [@routeAnimations]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</div>
</mat-drawer-content>
</mat-drawer-container>
<div @toolbar *ngIf="iamuser$ | async" class="admin-line" [ngClass]="{'expanded': !hideAdminWarn}"
matTooltip="IAM Administrator">
<button [matTooltip]="!hideAdminWarn ? 'Unpin': 'Pin'" (click)="toggleAdminHide()" mat-icon-button>
<mat-icon *ngIf="!hideAdminWarn" svgIcon="mdi_pin"></mat-icon>
<mat-icon *ngIf="hideAdminWarn" svgIcon="mdi_pin_outline"></mat-icon>
</button>
<span>{{'MENU.IAMADMIN' | translate}}</span>
</div>
</ng-container>
</ng-container>

View File

@ -66,13 +66,6 @@
}
}
.admin-line {
font-size: 12px;
padding: 4px 2rem;
position: relative;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: column;
@ -222,6 +215,13 @@
margin: .5rem 0;
flex: 1;
}
.hiddenline {
display: block;
visibility: hidden;
// flex: 1;
width: 4rem;
}
}
@mixin textvar($theme) {
@ -255,3 +255,8 @@
align-items: center;
}
}
.org-wrapper {
max-height: 350px;
overflow-y: auto;
}

View File

@ -1,10 +1,10 @@
import { async, TestBed } from '@angular/core/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],

View File

@ -23,7 +23,6 @@ import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service';
import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service';
@Component({
@ -57,13 +56,11 @@ export class AppComponent implements OnDestroy {
public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription();
public hideAdminWarn: boolean = true;
constructor(
public viewPortScroller: ViewportScroller,
@Inject('windowObject') public window: Window,
@ -73,15 +70,14 @@ export class AppComponent implements OnDestroy {
private breakpointObserver: BreakpointObserver,
public overlayContainer: OverlayContainer,
private themeService: ThemeService,
private mgmtService: ManagementService,
public mgmtService: ManagementService,
public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer,
private toast: ToastService,
private router: Router,
update: UpdateService,
@Inject(DOCUMENT) private document: Document,
) {
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px');
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px');
console.log('%cInserting something here could give attackers access to your zitadel account.', 'color: red; font-size: 18px');
console.log('%cIf you don\'t know exactly what you\'re doing, close the window and stay on the safe side', 'font-size: 16px');
console.log('%cIf you know exactly what you are doing, you should work for us', 'font-size: 16px');
@ -179,6 +175,8 @@ export class AppComponent implements OnDestroy {
value.trim().toLowerCase(),
);
});
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
}
public ngOnDestroy(): void {
@ -186,6 +184,11 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe();
}
public toggleAdminHide(): void {
this.hideAdminWarn = !this.hideAdminWarn;
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
}
public loadOrgs(filter?: string): void {
let query;
if (filter) {
@ -249,13 +252,9 @@ export class AppComponent implements OnDestroy {
private getProjectCount(): void {
this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => {
this.ownedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchProjects(0, 0);
this.mgmtService.SearchGrantedProjects(0, 0).then(res => {
this.grantedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchGrantedProjects(0, 0);
}
});
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AccountsCardComponent } from './accounts-card.component';
@ -6,7 +6,7 @@ describe('AccountsCardComponent', () => {
let component: AccountsCardComponent;
let fixture: ComponentFixture<AccountsCardComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AccountsCardComponent],
}).compileComponents();

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MemberCreateDialogComponent } from './member-create-dialog.component';
@ -7,7 +7,7 @@ describe('AddMemberDialogComponent', () => {
let component: MemberCreateDialogComponent;
let fixture: ComponentFixture<MemberCreateDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MemberCreateDialogComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AvatarComponent } from './avatar.component';
@ -6,7 +6,7 @@ describe('AvatarComponent', () => {
let component: AvatarComponent;
let fixture: ComponentFixture<AvatarComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AvatarComponent],
})

View File

@ -4,6 +4,7 @@
padding: 1.5rem;
border-radius: .5rem;
padding-top: 1rem;
min-width: 350px;
.header {
margin-top: 0;
@ -42,5 +43,6 @@
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CardComponent } from './card.component';
@ -6,7 +6,7 @@ describe('CardComponent', () => {
let component: CardComponent;
let fixture: ComponentFixture<CardComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CardComponent],
})

View File

@ -16,6 +16,7 @@
box-sizing: border-box;
border-radius: .5rem;
outline: none;
height: 100%;
.selection-icon {
opacity: 0;

View File

@ -5,6 +5,7 @@
margin-bottom: 1rem;
font-weight: 400;
margin-top: 1rem;
font-size: 14px;
}
@mixin changes-theme($theme) {

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ChangesComponent } from './changes.component';
@ -6,7 +6,7 @@ describe('ChangesComponent', () => {
let component: ChangesComponent;
let fixture: ComponentFixture<ChangesComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ChangesComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ContributorsComponent } from './contributors.component';
@ -6,7 +6,7 @@ describe('ContributorsComponent', () => {
let component: ContributorsComponent;
let fixture: ComponentFixture<ContributorsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ContributorsComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DetailLayoutComponent } from './detail-layout.component';
@ -6,7 +6,7 @@ describe('DetailLayoutComponent', () => {
let component: DetailLayoutComponent;
let fixture: ComponentFixture<DetailLayoutComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DetailLayoutComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IdpCreateComponent } from './idp-create.component';
@ -6,7 +6,7 @@ describe('IdpCreateComponent', () => {
let component: IdpCreateComponent;
let fixture: ComponentFixture<IdpCreateComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IdpCreateComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IdpTableComponent } from './idp-table.component';
@ -6,7 +6,7 @@ describe('UserTableComponent', () => {
let component: IdpTableComponent;
let fixture: ComponentFixture<IdpTableComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IdpTableComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IdpComponent } from './idp.component';
@ -6,7 +6,7 @@ describe('IdComponent', () => {
let component: IdpComponent;
let fixture: ComponentFixture<IdpComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IdpComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('MembersTableComponent', () => {
let component: MembersTableComponent;
let fixture: ComponentFixture<MembersTableComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MembersTableComponent],
imports: [

View File

@ -8,6 +8,7 @@
display: relative;
width: 100%;
overflow-y: auto;
padding-bottom: 50px;
&.hidden {
flex-basis: 100%;

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MetaLayoutComponent } from './meta-layout.component';
@ -6,7 +6,7 @@ describe('MetaLayoutComponent', () => {
let component: MetaLayoutComponent;
let fixture: ComponentFixture<MetaLayoutComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MetaLayoutComponent],
})

View File

@ -7,17 +7,10 @@
$primary-dark: mat-color($primary, A800);
/* stylelint-enable */
$lighter-color: rgba(mat-color($primary, 300), .5);
.meta-wrapper {
.meta {
position: relative;
flex: 1 0 300px;
background: linear-gradient(to bottom right, rgba($lighter-color, .05) 20%, transparent 50%);
&.hidden {
background: linear-gradient(to bottom right, rgba($lighter-color, .05), transparent 50%);
}
&::after {
border-left: 2px solid $primary-color;
@ -27,7 +20,7 @@
left top,
left bottom,
from($primary-color),
to($primary-dark),
to(rgb(0, 0, 0, .5)),
color-stop(
01,
$primary-dark
@ -39,7 +32,7 @@
left top,
left bottom,
from($primary-color),
to($primary-dark),
to(rgb(0, 0, 0, .5)),
color-stop(
01,
$primary-dark

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordComplexityViewComponent } from './password-complexity-view.component';
@ -6,7 +6,7 @@ describe('PasswordComplexityViewComponent', () => {
let component: PasswordComplexityViewComponent;
let fixture: ComponentFixture<PasswordComplexityViewComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordComplexityViewComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LabelPolicyComponent } from './label-policy.component';
@ -6,7 +6,7 @@ describe('LabelPolicyComponent', () => {
let component: LabelPolicyComponent;
let fixture: ComponentFixture<LabelPolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LabelPolicyComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddIdpDialogComponent } from './add-idp-dialog.component';
@ -7,7 +7,7 @@ describe('AddIdpDialogComponent', () => {
let component: AddIdpDialogComponent;
let fixture: ComponentFixture<AddIdpDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddIdpDialogComponent],
})

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LoginPolicyComponent } from './login-policy.component';
@ -6,7 +6,7 @@ describe('LoginPolicyComponent', () => {
let component: LoginPolicyComponent;
let fixture: ComponentFixture<LoginPolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LoginPolicyComponent],
})

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
@ -6,7 +6,7 @@ describe('OrgIamPolicyComponent', () => {
let component: OrgIamPolicyComponent;
let fixture: ComponentFixture<OrgIamPolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgIamPolicyComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordAgePolicyComponent } from './password-age-policy.component';
@ -6,7 +6,7 @@ describe('PasswordAgePolicyComponent', () => {
let component: PasswordAgePolicyComponent;
let fixture: ComponentFixture<PasswordAgePolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordAgePolicyComponent],
})

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
@ -6,7 +6,7 @@ describe('PasswordComplexityPolicyComponent', () => {
let component: PasswordComplexityPolicyComponent;
let fixture: ComponentFixture<PasswordComplexityPolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordComplexityPolicyComponent],
})

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
@ -6,7 +6,7 @@ describe('PasswordLockoutPolicyComponent', () => {
let component: PasswordLockoutPolicyComponent;
let fixture: ComponentFixture<PasswordLockoutPolicyComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordLockoutPolicyComponent],
})

View File

@ -16,8 +16,9 @@ h1 {
margin: .5rem;
display: flex;
flex-direction: column;
min-height: 200px;
min-height: 250px;
padding: 1rem;
height: 100%;
@media only screen and (max-width: 450px) {
flex-basis: 100%;

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PolicyGridComponent } from './policy-grid.component';
@ -6,7 +6,7 @@ describe('PolicyGridComponent', () => {
let component: PolicyGridComponent;
let fixture: ComponentFixture<PolicyGridComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PolicyGridComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('ProjectMembersComponent', () => {
let component: ProjectMembersComponent;
let fixture: ComponentFixture<ProjectMembersComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProjectMembersComponent],
imports: [

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProjectRoleDetailComponent } from './project-role-detail.component';
@ -6,7 +6,7 @@ describe('ProjectRoleDetailComponent', () => {
let component: ProjectRoleDetailComponent;
let fixture: ComponentFixture<ProjectRoleDetailComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProjectRoleDetailComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('ProjectRolesComponent', () => {
let component: ProjectRolesComponent;
let fixture: ComponentFixture<ProjectRolesComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProjectRolesComponent],
imports: [

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RefreshTableComponent } from './refresh-table.component';
@ -6,7 +6,7 @@ describe('RefreshTableComponent', () => {
let component: RefreshTableComponent;
let fixture: ComponentFixture<RefreshTableComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [RefreshTableComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchProjectAutocompleteComponent } from './search-project-autocomplete.component';
@ -7,7 +7,7 @@ describe('SearchProjectComponent', () => {
let component: SearchProjectAutocompleteComponent;
let fixture: ComponentFixture<SearchProjectAutocompleteComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchProjectAutocompleteComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchRolesAutocompleteComponent } from './search-roles-autocomplete.component';
@ -8,7 +8,7 @@ describe('SearchProjectComponent', () => {
let component: SearchRolesAutocompleteComponent;
let fixture: ComponentFixture<SearchRolesAutocompleteComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchRolesAutocompleteComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
@ -6,7 +6,7 @@ describe('SearchUserAutocompleteComponent', () => {
let component: SearchUserAutocompleteComponent;
let fixture: ComponentFixture<SearchUserAutocompleteComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchUserAutocompleteComponent],
})

View File

@ -103,7 +103,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
break;
default:
this.loadingSubject.next(true);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, []);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries ?? []);
this.loadResponse(promise3);
break;
}

View File

@ -1,6 +1,12 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection">
<mat-form-field @appearfade *ngIf="userGrantSearchKey != undefined" actions class="filtername">
<mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label>
<input matInput (keyup)="applyFilter($event)"
[placeholder]="('USER.TABLE.FILTER.' + userGrantSearchKey.toString()) | translate" #input>
</mat-form-field>
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i>
@ -33,19 +39,28 @@
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_ORG_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
@ -63,29 +78,32 @@
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>
{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_ROLE_KEY}"></template>
</th>
<td mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE">
<ng-container
*ngIf="(grant.grantId && loadedGrantId !== grant.grantId) || (loadedProjectId !== grant.projectId)">
<div class="flex-row">
<span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span>
<button mat-stroked-button
*ngIf="grant.grantId ? loadedGrantId !== grant.grantId : loadedProjectId !== grant.projectId"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(click)="grant.grantId ? getGrantRoleOptions(grant.grantId, grant.projectId) : getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
<ng-container
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
<div class="flex-row">
<div class="role">
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
</div>
</ng-container>
<span class="fill-space"></span>
<button mat-stroked-button
*ngIf="grant.grantId ? grantToEdit !== grant.id : grantToEdit !== grant.id"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
<ng-container
*ngIf="context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedProjectId && loadedProjectId === grant.projectId">
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.id && loadedProjectId && loadedProjectId === grant.projectId">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
@ -95,12 +113,15 @@
</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
<ng-container
*ngIf="context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedGrantId && loadedGrantId === grant.grantId">
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedGrantId && loadedGrantId === grant.grantId && grantToEdit == grant.id">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
@ -110,6 +131,10 @@
</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
</td>
</ng-container>
@ -123,4 +148,11 @@
[length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div>
</app-refresh-table>
</app-refresh-table>
<ng-template #templateRef let-key="key">
<button class="search-button" mat-icon-button (click)="setFilter(key)">
<mat-icon *ngIf="this.userGrantSearchKey != key">search</mat-icon>
<mat-icon *ngIf="this.userGrantSearchKey == key">search_off</mat-icon>
</button>
</ng-template>

View File

@ -21,6 +21,19 @@
}
}
th {
.search-button {
display: none;
}
&:hover,
&.search-active {
.search-button {
display: inline-block;
}
}
}
.selection {
width: 50px;
max-width: 50px;
@ -35,15 +48,22 @@
.flex-row {
display: flex;
flex-direction: column;
flex-direction: row;
max-width: 400px;
.role {
display: block;
display: flex;
flex-direction: column;
margin: .25rem;
font-size: 14px;
text-overflow: ellipsis;
overflow: hidden;
justify-items: center;
align-self: center;
}
.fill-space {
flex: 1;
}
button {
@ -60,3 +80,7 @@
.fill-space {
flex: 1;
}
.filtername {
margin-right: 1rem;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UserGrantsComponent } from './user-grants.component';
@ -6,7 +6,7 @@ describe('UserGrantsComponent', () => {
let component: UserGrantsComponent;
let fixture: ComponentFixture<UserGrantsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [UserGrantsComponent],
})

View File

@ -1,10 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material/input';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { ProjectRoleView, UserGrant, UserGrantView } from 'src/app/proto/generated/management_pb';
import { enterAnimations } from 'src/app/animations';
import {
ProjectRoleView,
SearchMethod,
UserGrant,
UserGrantSearchKey,
UserGrantSearchQuery,
UserGrantView,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@ -14,12 +23,17 @@ import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource
selector: 'app-user-grants',
templateUrl: './user-grants.component.html',
styleUrls: ['./user-grants.component.scss'],
animations: [
enterAnimations,
],
})
export class UserGrantsComponent implements OnInit, AfterViewInit {
public userGrantSearchKey: UserGrantSearchKey | undefined = undefined;
public UserGrantSearchKey: any = UserGrantSearchKey;
public INITIAL_PAGE_SIZE: number = 50;
@Input() context: UserGrantContext = UserGrantContext.NONE;
@Input() refreshOnPreviousRoutes: string[] = [];
public grants: UserGrantView.AsObject[] = [];
public dataSource!: UserGrantsDataSource;
public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []);
@ -32,6 +46,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() userId: string = '';
@Input() projectId: string = '';
@Input() grantId: string = '';
@ViewChild('input') public filter!: MatInput;
public grantRoleOptions: string[] = [];
public projectRoleOptions: ProjectRoleView.AsObject[] = [];
@ -39,6 +54,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public loadedGrantId: string = '';
public loadedProjectId: string = '';
public grantToEdit: string = '';
public UserGrantContext: any = UserGrantContext;
@ -89,7 +105,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
.subscribe();
}
private loadGrantsPage(): void {
private loadGrantsPage(filterValue?: string): void {
let queries: UserGrantSearchQuery[] = [];
if (this.userGrantSearchKey !== undefined && filterValue) {
const query = new UserGrantSearchQuery();
query.setKey(this.userGrantSearchKey);
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
query.setValue(filterValue);
queries = [query];
}
this.dataSource.loadGrants(
this.context,
this.paginator?.pageIndex ?? 0,
@ -99,6 +124,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
grantId: this.grantId,
userId: this.userId,
},
queries,
);
}
@ -114,7 +140,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row));
}
public getGrantRoleOptions(grantId: string, projectId: string): void {
public loadGrantOptions(grant: UserGrantView.AsObject): void {
this.grantToEdit = grant.id;
if (grant.grantId && grant.projectId) {
this.getGrantRoleOptions(grant.grantId, grant.projectId);
} else if (grant.projectId) {
this.getProjectRoleOptions(grant.projectId);
}
}
private getGrantRoleOptions(grantId: string, projectId: string): void {
this.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => {
this.loadedGrantId = grantId;
this.grantRoleOptions = resp.toObject().roleKeysList;
@ -123,7 +158,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
});
}
public getProjectRoleOptions(projectId: string): void {
private getProjectRoleOptions(projectId: string): void {
this.mgmtService.SearchProjectRoles(projectId, 100, 0).then(resp => {
this.loadedProjectId = projectId;
this.projectRoleOptions = resp.toObject().resultList;
@ -168,4 +203,26 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
},
);
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
this.loadGrantsPage(filterValue);
}
public setFilter(key: UserGrantSearchKey): void {
setTimeout(() => {
if (this.filter) {
(this.filter as any).nativeElement.focus();
}
}, 100);
if (this.userGrantSearchKey !== key) {
this.userGrantSearchKey = key;
} else {
this.userGrantSearchKey = undefined;
this.loadGrantsPage();
}
}
}

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
@ -39,6 +40,7 @@ import { UserGrantsComponent } from './user-grants.component';
MatCheckboxModule,
MatTooltipModule,
MatSelectModule,
MatInputModule,
MatFormFieldModule,
TranslateModule,
HasRolePipeModule,

View File

@ -11,6 +11,10 @@
.action {
display: flex;
button {
border-radius: .5rem;
}
.ok-button {
margin-left: .5rem;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { WarnDialogComponent } from './warn-dialog.component';
@ -6,7 +6,7 @@ describe('WarnDialogComponent', () => {
let component: WarnDialogComponent;
let fixture: ComponentFixture<WarnDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [WarnDialogComponent],
})

View File

@ -24,6 +24,23 @@
{{'HOME.IAM'| translate}}</h2>
<p>{{'HOME.IAM_DESC'| translate}}</p>
</div>
<ng-template appHasRole [appHasRole]="['org.create','iam.write']">
<a class="short-link" [routerLink]="[ '/org', 'create']">{{'HOME.IAM_CREATE_ORG' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<a class="short-link"
[routerLink]="[ '/iam', 'policy','iam']">{{'HOME.IAM_POLICY_IAM' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/iam', 'policy','complexity']">{{'HOME.IAM_POLICY_COMPLEXITY' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/iam', 'policy','login']">{{'HOME.IAM_POLICY_LOGIN' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<span class="fill-space"></span>
<div class="footer">
<a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a>
@ -37,6 +54,9 @@
{{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
</div>
<a class="short-link" [routerLink]="[ '/users', 'me','password']">{{'HOME.CHANGE_PWD' | translate}}<i
class="las la-link"></i></a>
<span class="fill-space"></span>
<div class="footer">
<a color="primary" mat-stroked-button
@ -51,6 +71,11 @@
<i class="icon las la-layer-group"></i>
{{'HOME.PROJECTS'| translate}}</h2>
<p>{{'HOME.PROJECTS_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['project.create']">
<a class="short-link"
[routerLink]="[ '/projects', 'create']">{{'HOME.PROJECTS_NEW_LINK' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">
@ -66,6 +91,19 @@
<h2> <i class="icon las la-archway"></i>
{{'HOME.PROTECTION'| translate}}</h2>
<p>{{'HOME.PROTECTION_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<a class="short-link"
[routerLink]="[ '/org', 'policy','iam']">{{'HOME.ORG_POLICY_IAM' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.read']">
<a class="short-link"
[routerLink]="[ '/org', 'policy','complexity']">{{'HOME.ORG_POLICY_COMPLEXITY' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/org', 'policy','login']">{{'HOME.ORG_POLICY_LOGIN' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">
@ -82,6 +120,18 @@
<i class="las la-users"></i>
{{'HOME.USERS'| translate}}</h2>
<p>{{'HOME.USERS_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['user.read(:[0-9]*)?']">
<a class="short-link"
[routerLink]="[ '/users', 'list', 'humans']">{{'HOME.USERS_HUMANS' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/users', 'list', 'machines']">{{'HOME.USERS_MACHINES' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.read']">
<a class="short-link" [routerLink]="[ '/users', 'create']">{{'HOME.USERS_CREATE' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">

View File

@ -32,7 +32,7 @@
justify-content: space-evenly;
.item {
flex: 1 1 45%;
flex: 1 0 45%;
margin: 0 1rem;
display: flex;
flex-direction: column;
@ -80,3 +80,25 @@
margin-top: 3rem;
}
}
.short-link {
margin-bottom: .5rem;
font-size: 15px;
text-decoration: none;
display: block;
position: relative;
.las {
font-size: 1.5rem !important;
height: 1.5rem;
visibility: hidden;
position: absolute;
right: 0;
}
&:hover {
.las {
visibility: visible;
}
}
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { HomeComponent } from './home.component';
@ -6,7 +6,7 @@ describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HomeComponent],
}).compileComponents();

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FailedEventsComponent } from './failed-events.component';
@ -6,7 +6,7 @@ describe('FailedEventsComponent', () => {
let component: FailedEventsComponent;
let fixture: ComponentFixture<FailedEventsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FailedEventsComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('IamMembersComponent', () => {
let component: IamMembersComponent;
let fixture: ComponentFixture<IamMembersComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IamMembersComponent],
imports: [

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IamViewsComponent } from './iam-views.component';
@ -6,7 +6,7 @@ describe('IamViewsComponent', () => {
let component: IamViewsComponent;
let fixture: ComponentFixture<IamViewsComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IamViewsComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IamComponent } from './iam.component';
@ -6,7 +6,7 @@ describe('IamComponent', () => {
let component: IamComponent;
let fixture: ComponentFixture<IamComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IamComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgCreateComponent } from './org-create.component';
@ -6,7 +6,7 @@ describe('OrgCreateComponent', () => {
let component: OrgCreateComponent;
let fixture: ComponentFixture<OrgCreateComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgCreateComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddDomainDialogComponent } from './add-domain-dialog.component';
@ -6,7 +6,7 @@ describe('WarnDialogComponent', () => {
let component: AddDomainDialogComponent;
let fixture: ComponentFixture<AddDomainDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddDomainDialogComponent],
})

View File

@ -12,8 +12,10 @@
{{'ORG.PAGES.ORGDOMAIN.TYPES.'+ domain.validationType | translate}}</p>
<div *ngIf="domain.validationType !== OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_UNSPECIFIED"
class="btn-container">
<button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)"
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)" (click)="validate()">
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
<button *ngIf="!showNew" mat-stroked-button color="primary"
(click)="showNew = true">{{'ORG.PAGES.ORGDOMAIN.REQUESTNEWTOKEN' | translate}}</button>
@ -34,8 +36,11 @@
<div class="btn-container">
<button mat-stroked-button (click)="saveFile()"
color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" type="submit" mat-raised-button
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" class="verify-button" type="submit" mat-raised-button (click)="validate()">
<span>{{ 'ACTIONS.VERIFY' | translate }}</span>
<mat-spinner class="spinner" *ngIf="!validating" diameter="20" mode="indeterminate"></mat-spinner>
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
</div>
@ -48,8 +53,10 @@
<i *ngIf="copied != dns.token" class="las la-clipboard"></i>
<i *ngIf="copied == dns.token" class="las la-clipboard-check"></i>
</button>
<button color="primary" type="submit" mat-raised-button
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" type="submit" mat-raised-button class="verify-button" (click)="validate()">
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
<p class="entry">{{dns?.url}}</p>
</div>

View File

@ -1,10 +1,10 @@
.btn-container {
display: flex;
margin: -.5rem;
align-items: center;
button {
margin: 1rem .5rem;
display: block;
}
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DomainVerificationComponent } from './domain-verification.component';
@ -6,7 +6,7 @@ describe('DomainVerificationComponent', () => {
let component: DomainVerificationComponent;
let fixture: ComponentFixture<DomainVerificationComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DomainVerificationComponent],
})

View File

@ -21,6 +21,7 @@ export class DomainVerificationComponent {
public showNew: boolean = false;
public validating: boolean = false;
constructor(
private toast: ToastService,
public dialogRef: MatDialogRef<DomainVerificationComponent>,
@ -54,10 +55,14 @@ export class DomainVerificationComponent {
}
public validate(): void {
this.validating = true;
this.mgmtService.ValidateMyOrgDomain(this.domain.domain).then(() => {
this.dialogRef.close(false);
this.dialogRef.close(true);
this.toast.showInfo('ORG.PAGES.ORGDOMAIN.VERIFICATION_SUCCESSFUL', true);
this.validating = false;
}).catch((error) => {
this.toast.showError(error);
this.validating = false;
});
}

View File

@ -36,7 +36,7 @@
.verified,
.primary {
color: #5282c1;
color: var(--color-main);
margin-right: 1rem;
}

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgDetailComponent } from './org-detail.component';
@ -6,7 +6,7 @@ describe('OrgDetailComponent', () => {
let component: OrgDetailComponent;
let fixture: ComponentFixture<OrgDetailComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgDetailComponent],
})

View File

@ -79,10 +79,12 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
}).catch(error => {
this.toast.showError(error);
});
this.loadMembers();
this.loadDomains();
}
this.mgmtService.SearchMyOrgDomains(0, 100).then(result => {
public loadDomains(): void {
this.mgmtService.SearchMyOrgDomains().then(result => {
this.domains = result.toObject().resultList;
this.primaryDomain = this.domains.find(domain => domain.primary)?.domain ?? '';
});
@ -91,7 +93,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
public setPrimary(domain: OrgDomainView.AsObject): void {
this.mgmtService.setMyPrimaryOrgDomain(domain.domain).then(() => {
this.toast.showInfo('ORG.TOAST.SETPRIMARY', true);
this.getData();
this.loadDomains();
}).catch((error) => {
this.toast.showError(error);
});
@ -202,12 +204,18 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
}
public verifyDomain(domain: OrgDomainView.AsObject): void {
this.dialog.open(DomainVerificationComponent, {
const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: {
domain: domain,
},
width: '500px',
});
dialogRef.afterClosed().subscribe((reload) => {
if (reload) {
this.loadDomains();
}
});
}
public loadMembers(): void {

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgListComponent } from './org-list.component';
@ -6,7 +6,7 @@ describe('OrgListComponent', () => {
let component: OrgListComponent;
let fixture: ComponentFixture<OrgListComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgListComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgMemberRolesAutocompleteComponent } from './org-member-roles-autocomplete.component';
@ -6,7 +6,7 @@ describe('OrgMemberRolesAutocompleteComponent', () => {
let component: OrgMemberRolesAutocompleteComponent;
let fixture: ComponentFixture<OrgMemberRolesAutocompleteComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgMemberRolesAutocompleteComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('OrgMembersComponent', () => {
let component: OrgMembersComponent;
let fixture: ComponentFixture<OrgMembersComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgMembersComponent],
imports: [

View File

@ -8,6 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
@ -51,6 +52,7 @@ import { OrgsRoutingModule } from './orgs-routing.module';
MemberCreateDialogModule,
MatMenuModule,
ChangesModule,
MatProgressSpinnerModule,
AddDomainDialogModule,
TranslateModule,
SharedModule,

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AppCreateComponent } from './app-create.component';
@ -6,7 +6,7 @@ describe('AppCreateComponent', () => {
let component: AppCreateComponent;
let fixture: ComponentFixture<AppCreateComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AppCreateComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AppDetailComponent } from './app-detail.component';
@ -6,7 +6,7 @@ describe('AppDetailComponent', () => {
let component: AppDetailComponent;
let fixture: ComponentFixture<AppDetailComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AppDetailComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AppSecretDialogComponent } from './app-secret-dialog.component';
@ -6,7 +6,7 @@ describe('AppSecretDialogComponent', () => {
let component: AppSecretDialogComponent;
let fixture: ComponentFixture<AppSecretDialogComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AppSecretDialogComponent],
})

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectDetailComponent } from './granted-project-detail.component';
@ -6,7 +6,7 @@ describe('GrantedProjectDetailComponent', () => {
let component: GrantedProjectDetailComponent;
let fixture: ComponentFixture<GrantedProjectDetailComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectDetailComponent],
})

View File

@ -22,11 +22,10 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
</div>
@ -47,12 +46,20 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<button [ngClass]="{ selected: selection.isSelected(item)}"
(click)="selection.toggle(item); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
</div>
<ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}"
(click)="selection.toggle(key); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button>
</ng-template>

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectGridComponent } from './granted-project-grid.component';
@ -6,7 +6,7 @@ describe('GridComponent', () => {
let component: GrantedProjectGridComponent;
let fixture: ComponentFixture<GrantedProjectGridComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectGridComponent],
})

View File

@ -47,11 +47,10 @@ export class GrantedProjectGridComponent implements OnChanges {
this.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item));
filtered.forEach((f, i) => {
this.notPinned.splice(i, 1);
selection.added.forEach(item => {
const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(index, 1);
});
this.notPinned.push(...selection.removed);
});
});
@ -74,18 +73,10 @@ export class GrantedProjectGridComponent implements OnChanges {
const array: string[] = JSON.parse(storageEntry);
const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (array.includes(item.projectId)) {
// this.notPinned.splice(index, 1);
return true;
}
});
this.selection.select(...toSelect);
const toNotPinned: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (!array.includes(item.projectId)) {
return true;
}
});
this.notPinned = toNotPinned;
}
});
}

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