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 prefix: chore
include: scope include: scope
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/build/docker" directory: "/build/"
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 10 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' GO_VERSION: '1.15'
jobs: jobs:
container:
## Angular test, will be added later
angular-lint:
runs-on: ubuntu-18.04 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: steps:
- name: Source checkout - name: Source checkout
uses: actions/checkout@v2 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: with:
name: zitadel-linux-amd64 path: /tmp/.buildx-cache
path: .artifacts key: ${{ runner.os }}-buildx-${{ github.sha }}
- uses: docker/build-push-action@v1 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: with:
dockerfile: build/docker/Dockerfile
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.CR_PAT }} password: ${{ secrets.CR_PAT }}
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
repository: ${{ github.repository }} - uses: docker/build-push-action@v2
tag_with_ref: true with:
tag_with_sha: true 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: release:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
needs: [container-prod] needs: [container]
env: env:
DOCKER_USERNAME: ${{ github.actor }} DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.CR_PAT }} DOCKER_PASSWORD: ${{ secrets.CR_PAT }}
@ -139,32 +60,32 @@ jobs:
- name: Generate Short SHA Container Tag - name: Generate Short SHA Container Tag
id: vars id: vars
run: echo "::set-output name=sha_short::SHA-$(git rev-parse --short HEAD)" 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 - name: Docker Login
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
- name: Docker Pull short-sha - name: Docker Pull short-sha
run: docker pull $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} run: docker pull $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }}
- name: Semantic Release - name: Semantic Release
id: semantic
uses: cycjimmy/semantic-release-action@v2 uses: cycjimmy/semantic-release-action@v2
with: with:
dry_run: false dry_run: false
semantic_version: 17.0.4 semantic_version: 17.0.4
extra_plugins: | - name: Do something when a new release published
@semantic-release/exec@5.0.0 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 - name: Docker Tag Version
run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY:$CAOS_NEXT_VERSION run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY:${{ steps.semantic.outputs.new_release_version }}
if: env.CAOS_NEXT_VERSION != '' if: steps.semantic.outputs.new_release_published == 'true'
- name: Docker Tag Latest - name: Docker Tag Latest
run: docker tag $REGISTRY/$GITHUB_REPOSITORY:${{ steps.vars.outputs.sha_short }} $REGISTRY/$GITHUB_REPOSITORY: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 - name: Docker Push Version
run: docker push $REGISTRY/$GITHUB_REPOSITORY:$CAOS_NEXT_VERSION run: docker push $REGISTRY/$GITHUB_REPOSITORY:${{ steps.semantic.outputs.new_release_version }}
if: env.CAOS_NEXT_VERSION != '' if: steps.semantic.outputs.new_release_published == 'true'
- name: Docker Push Latest - name: Docker Push Latest
run: docker push $REGISTRY/$GITHUB_REPOSITORY: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 # buildfolders and generated js
tmp/ 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: [ plugins: [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
["@semantic-release/github", { "@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}'"
}],
] ]
}; };

View File

@ -2,6 +2,10 @@
## **Did you find a bug?** ## **Did you find a bug?**
## **Want to contribute code?**
* Check out our [Dev Build Guide](build/README.md).
## **Did you find a security flaw?** ## **Did you find a security flaw?**
* Please read [Security Policy](SECURITY.md). * 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) 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. 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 GEN_PATH=src/app/proto/generated
echo "Remove old files"
rm -rf $GEN_PATH
echo "Create folders" echo "Create folders"
mkdir -p $GEN_PATH 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" echo "Generate grpc"
protoc \ protoc \
-I=/usr/local/include \ -I=.tmp/protos/message \
-I=../pkg/grpc/message \ -I=.tmp/protos/admin/proto \
-I=../pkg/grpc/management/proto \ -I=.tmp/protos/management/proto \
-I=../pkg/grpc/auth/proto \ -I=.tmp/protos/auth/proto \
-I=../pkg/grpc/admin/proto \
-I=../internal/protoc/protoc-gen-authoption \
-I=node_modules/google-proto-files \ -I=node_modules/google-proto-files \
-I=tmp \ -I=.tmp/protos \
--js_out=import_style=commonjs,binary:$GEN_PATH \ --js_out=import_style=commonjs,binary:$GEN_PATH \
--grpc-web_out=import_style=commonjs+dts,mode=grpcweb:$GEN_PATH \ --grpc-web_out=import_style=commonjs+dts,mode=grpcweb:$GEN_PATH \
../pkg/grpc/message/proto/*.proto \ .tmp/protos/message/proto/*.proto \
../pkg/grpc/management/proto/*.proto \ .tmp/protos/admin/proto/*.proto \
../pkg/grpc/admin/proto/*.proto \ .tmp/protos/auth/proto/*.proto \
../pkg/grpc/auth/proto/*.proto .tmp/protos/management/proto/*.proto
echo "Generate annotations js file (compatibility)" 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 PasswordCheck: 240h #10d
ExternalLoginCheck: 240h #10d ExternalLoginCheck: 240h #10d
MfaInitSkip: 720h #30d MfaInitSkip: 720h #30d
MfaSoftwareCheck: 18h SecondFactorCheck: 18h
MfaHardwareCheck: 12h MultiFactorCheck: 12h
IamID: 'IAM' IamID: 'IAM'
DomainVerification: DomainVerification:
VerificationKey: VerificationKey:

View File

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

View File

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

View File

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

View File

@ -66,13 +66,6 @@
} }
} }
.admin-line {
font-size: 12px;
padding: 4px 2rem;
position: relative;
overflow: hidden;
}
.main-container { .main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -222,6 +215,13 @@
margin: .5rem 0; margin: .5rem 0;
flex: 1; flex: 1;
} }
.hiddenline {
display: block;
visibility: hidden;
// flex: 1;
width: 4rem;
}
} }
@mixin textvar($theme) { @mixin textvar($theme) {
@ -255,3 +255,8 @@
align-items: center; 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 { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule], imports: [RouterTestingModule],
declarations: [AppComponent], declarations: [AppComponent],

View File

@ -23,7 +23,6 @@ import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service'; import { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service'; import { ManagementService } from './services/mgmt.service';
import { ThemeService } from './services/theme.service'; import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service'; import { UpdateService } from './services/update.service';
@Component({ @Component({
@ -57,13 +56,11 @@ export class AppComponent implements OnDestroy {
public showProjectSection: boolean = false; public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl(''); public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription(); private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription(); private orgSub: Subscription = new Subscription();
public hideAdminWarn: boolean = true;
constructor( constructor(
public viewPortScroller: ViewportScroller, public viewPortScroller: ViewportScroller,
@Inject('windowObject') public window: Window, @Inject('windowObject') public window: Window,
@ -73,15 +70,14 @@ export class AppComponent implements OnDestroy {
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
public overlayContainer: OverlayContainer, public overlayContainer: OverlayContainer,
private themeService: ThemeService, private themeService: ThemeService,
private mgmtService: ManagementService, public mgmtService: ManagementService,
public matIconRegistry: MatIconRegistry, public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer, public domSanitizer: DomSanitizer,
private toast: ToastService,
private router: Router, private router: Router,
update: UpdateService, update: UpdateService,
@Inject(DOCUMENT) private document: Document, @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('%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 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'); 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(), value.trim().toLowerCase(),
); );
}); });
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
@ -186,6 +184,11 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe(); this.orgSub.unsubscribe();
} }
public toggleAdminHide(): void {
this.hideAdminWarn = !this.hideAdminWarn;
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
}
public loadOrgs(filter?: string): void { public loadOrgs(filter?: string): void {
let query; let query;
if (filter) { if (filter) {
@ -249,13 +252,9 @@ export class AppComponent implements OnDestroy {
private getProjectCount(): void { private getProjectCount(): void {
this.authService.isAllowed(['project.read$']).subscribe((allowed) => { this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) { if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => { this.mgmtService.SearchProjects(0, 0);
this.ownedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchGrantedProjects(0, 0).then(res => { this.mgmtService.SearchGrantedProjects(0, 0);
this.grantedProjectsCount = res.toObject().totalResult;
});
} }
}); });
} }

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'; import { AccountsCardComponent } from './accounts-card.component';
@ -6,7 +6,7 @@ describe('AccountsCardComponent', () => {
let component: AccountsCardComponent; let component: AccountsCardComponent;
let fixture: ComponentFixture<AccountsCardComponent>; let fixture: ComponentFixture<AccountsCardComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AccountsCardComponent], declarations: [AccountsCardComponent],
}).compileComponents(); }).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'; import { MemberCreateDialogComponent } from './member-create-dialog.component';
@ -7,7 +7,7 @@ describe('AddMemberDialogComponent', () => {
let component: MemberCreateDialogComponent; let component: MemberCreateDialogComponent;
let fixture: ComponentFixture<MemberCreateDialogComponent>; let fixture: ComponentFixture<MemberCreateDialogComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [MemberCreateDialogComponent], 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'; import { AvatarComponent } from './avatar.component';
@ -6,7 +6,7 @@ describe('AvatarComponent', () => {
let component: AvatarComponent; let component: AvatarComponent;
let fixture: ComponentFixture<AvatarComponent>; let fixture: ComponentFixture<AvatarComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AvatarComponent], declarations: [AvatarComponent],
}) })

View File

@ -4,6 +4,7 @@
padding: 1.5rem; padding: 1.5rem;
border-radius: .5rem; border-radius: .5rem;
padding-top: 1rem; padding-top: 1rem;
min-width: 350px;
.header { .header {
margin-top: 0; margin-top: 0;
@ -42,5 +43,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; 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'; import { CardComponent } from './card.component';
@ -6,7 +6,7 @@ describe('CardComponent', () => {
let component: CardComponent; let component: CardComponent;
let fixture: ComponentFixture<CardComponent>; let fixture: ComponentFixture<CardComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [CardComponent], declarations: [CardComponent],
}) })

View File

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

View File

@ -5,6 +5,7 @@
margin-bottom: 1rem; margin-bottom: 1rem;
font-weight: 400; font-weight: 400;
margin-top: 1rem; margin-top: 1rem;
font-size: 14px;
} }
@mixin changes-theme($theme) { @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'; import { ChangesComponent } from './changes.component';
@ -6,7 +6,7 @@ describe('ChangesComponent', () => {
let component: ChangesComponent; let component: ChangesComponent;
let fixture: ComponentFixture<ChangesComponent>; let fixture: ComponentFixture<ChangesComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ChangesComponent], 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'; import { ContributorsComponent } from './contributors.component';
@ -6,7 +6,7 @@ describe('ContributorsComponent', () => {
let component: ContributorsComponent; let component: ContributorsComponent;
let fixture: ComponentFixture<ContributorsComponent>; let fixture: ComponentFixture<ContributorsComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ContributorsComponent], 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'; import { DetailLayoutComponent } from './detail-layout.component';
@ -6,7 +6,7 @@ describe('DetailLayoutComponent', () => {
let component: DetailLayoutComponent; let component: DetailLayoutComponent;
let fixture: ComponentFixture<DetailLayoutComponent>; let fixture: ComponentFixture<DetailLayoutComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DetailLayoutComponent], 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'; import { IdpCreateComponent } from './idp-create.component';
@ -6,7 +6,7 @@ describe('IdpCreateComponent', () => {
let component: IdpCreateComponent; let component: IdpCreateComponent;
let fixture: ComponentFixture<IdpCreateComponent>; let fixture: ComponentFixture<IdpCreateComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IdpCreateComponent], 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'; import { IdpTableComponent } from './idp-table.component';
@ -6,7 +6,7 @@ describe('UserTableComponent', () => {
let component: IdpTableComponent; let component: IdpTableComponent;
let fixture: ComponentFixture<IdpTableComponent>; let fixture: ComponentFixture<IdpTableComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IdpTableComponent], 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'; import { IdpComponent } from './idp.component';
@ -6,7 +6,7 @@ describe('IdComponent', () => {
let component: IdpComponent; let component: IdpComponent;
let fixture: ComponentFixture<IdpComponent>; let fixture: ComponentFixture<IdpComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IdpComponent], 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 { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('MembersTableComponent', () => {
let component: MembersTableComponent; let component: MembersTableComponent;
let fixture: ComponentFixture<MembersTableComponent>; let fixture: ComponentFixture<MembersTableComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [MembersTableComponent], declarations: [MembersTableComponent],
imports: [ imports: [

View File

@ -8,6 +8,7 @@
display: relative; display: relative;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;
padding-bottom: 50px;
&.hidden { &.hidden {
flex-basis: 100%; 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'; import { MetaLayoutComponent } from './meta-layout.component';
@ -6,7 +6,7 @@ describe('MetaLayoutComponent', () => {
let component: MetaLayoutComponent; let component: MetaLayoutComponent;
let fixture: ComponentFixture<MetaLayoutComponent>; let fixture: ComponentFixture<MetaLayoutComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [MetaLayoutComponent], declarations: [MetaLayoutComponent],
}) })

View File

@ -7,17 +7,10 @@
$primary-dark: mat-color($primary, A800); $primary-dark: mat-color($primary, A800);
/* stylelint-enable */ /* stylelint-enable */
$lighter-color: rgba(mat-color($primary, 300), .5);
.meta-wrapper { .meta-wrapper {
.meta { .meta {
position: relative; position: relative;
flex: 1 0 300px; 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 { &::after {
border-left: 2px solid $primary-color; border-left: 2px solid $primary-color;
@ -27,7 +20,7 @@
left top, left top,
left bottom, left bottom,
from($primary-color), from($primary-color),
to($primary-dark), to(rgb(0, 0, 0, .5)),
color-stop( color-stop(
01, 01,
$primary-dark $primary-dark
@ -39,7 +32,7 @@
left top, left top,
left bottom, left bottom,
from($primary-color), from($primary-color),
to($primary-dark), to(rgb(0, 0, 0, .5)),
color-stop( color-stop(
01, 01,
$primary-dark $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'; import { PasswordComplexityViewComponent } from './password-complexity-view.component';
@ -6,7 +6,7 @@ describe('PasswordComplexityViewComponent', () => {
let component: PasswordComplexityViewComponent; let component: PasswordComplexityViewComponent;
let fixture: ComponentFixture<PasswordComplexityViewComponent>; let fixture: ComponentFixture<PasswordComplexityViewComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordComplexityViewComponent], 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'; import { LabelPolicyComponent } from './label-policy.component';
@ -6,7 +6,7 @@ describe('LabelPolicyComponent', () => {
let component: LabelPolicyComponent; let component: LabelPolicyComponent;
let fixture: ComponentFixture<LabelPolicyComponent>; let fixture: ComponentFixture<LabelPolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LabelPolicyComponent], 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'; import { AddIdpDialogComponent } from './add-idp-dialog.component';
@ -7,7 +7,7 @@ describe('AddIdpDialogComponent', () => {
let component: AddIdpDialogComponent; let component: AddIdpDialogComponent;
let fixture: ComponentFixture<AddIdpDialogComponent>; let fixture: ComponentFixture<AddIdpDialogComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AddIdpDialogComponent], declarations: [AddIdpDialogComponent],
}) })

View File

@ -1,5 +1,5 @@
.default { .default {
color: #5282c1; color: var(--color-main);
margin-top: 0; 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'; import { LoginPolicyComponent } from './login-policy.component';
@ -6,7 +6,7 @@ describe('LoginPolicyComponent', () => {
let component: LoginPolicyComponent; let component: LoginPolicyComponent;
let fixture: ComponentFixture<LoginPolicyComponent>; let fixture: ComponentFixture<LoginPolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LoginPolicyComponent], declarations: [LoginPolicyComponent],
}) })

View File

@ -1,5 +1,5 @@
.default { .default {
color: #5282c1; color: var(--color-main);
margin-top: 0; 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'; import { OrgIamPolicyComponent } from './org-iam-policy.component';
@ -6,7 +6,7 @@ describe('OrgIamPolicyComponent', () => {
let component: OrgIamPolicyComponent; let component: OrgIamPolicyComponent;
let fixture: ComponentFixture<OrgIamPolicyComponent>; let fixture: ComponentFixture<OrgIamPolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgIamPolicyComponent], 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'; import { PasswordAgePolicyComponent } from './password-age-policy.component';
@ -6,7 +6,7 @@ describe('PasswordAgePolicyComponent', () => {
let component: PasswordAgePolicyComponent; let component: PasswordAgePolicyComponent;
let fixture: ComponentFixture<PasswordAgePolicyComponent>; let fixture: ComponentFixture<PasswordAgePolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordAgePolicyComponent], declarations: [PasswordAgePolicyComponent],
}) })

View File

@ -1,5 +1,5 @@
.default { .default {
color: #5282c1; color: var(--color-main);
margin-top: 0; 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'; import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
@ -6,7 +6,7 @@ describe('PasswordComplexityPolicyComponent', () => {
let component: PasswordComplexityPolicyComponent; let component: PasswordComplexityPolicyComponent;
let fixture: ComponentFixture<PasswordComplexityPolicyComponent>; let fixture: ComponentFixture<PasswordComplexityPolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordComplexityPolicyComponent], declarations: [PasswordComplexityPolicyComponent],
}) })

View File

@ -1,5 +1,5 @@
.default { .default {
color: #5282c1; color: var(--color-main);
margin-top: 0; 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'; import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
@ -6,7 +6,7 @@ describe('PasswordLockoutPolicyComponent', () => {
let component: PasswordLockoutPolicyComponent; let component: PasswordLockoutPolicyComponent;
let fixture: ComponentFixture<PasswordLockoutPolicyComponent>; let fixture: ComponentFixture<PasswordLockoutPolicyComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordLockoutPolicyComponent], declarations: [PasswordLockoutPolicyComponent],
}) })

View File

@ -16,8 +16,9 @@ h1 {
margin: .5rem; margin: .5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 200px; min-height: 250px;
padding: 1rem; padding: 1rem;
height: 100%;
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
flex-basis: 100%; 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'; import { PolicyGridComponent } from './policy-grid.component';
@ -6,7 +6,7 @@ describe('PolicyGridComponent', () => {
let component: PolicyGridComponent; let component: PolicyGridComponent;
let fixture: ComponentFixture<PolicyGridComponent>; let fixture: ComponentFixture<PolicyGridComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PolicyGridComponent], 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 { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('ProjectMembersComponent', () => {
let component: ProjectMembersComponent; let component: ProjectMembersComponent;
let fixture: ComponentFixture<ProjectMembersComponent>; let fixture: ComponentFixture<ProjectMembersComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectMembersComponent], declarations: [ProjectMembersComponent],
imports: [ 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'; import { ProjectRoleDetailComponent } from './project-role-detail.component';
@ -6,7 +6,7 @@ describe('ProjectRoleDetailComponent', () => {
let component: ProjectRoleDetailComponent; let component: ProjectRoleDetailComponent;
let fixture: ComponentFixture<ProjectRoleDetailComponent>; let fixture: ComponentFixture<ProjectRoleDetailComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectRoleDetailComponent], 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 { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('ProjectRolesComponent', () => {
let component: ProjectRolesComponent; let component: ProjectRolesComponent;
let fixture: ComponentFixture<ProjectRolesComponent>; let fixture: ComponentFixture<ProjectRolesComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectRolesComponent], declarations: [ProjectRolesComponent],
imports: [ 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'; import { RefreshTableComponent } from './refresh-table.component';
@ -6,7 +6,7 @@ describe('RefreshTableComponent', () => {
let component: RefreshTableComponent; let component: RefreshTableComponent;
let fixture: ComponentFixture<RefreshTableComponent>; let fixture: ComponentFixture<RefreshTableComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [RefreshTableComponent], 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'; import { SearchProjectAutocompleteComponent } from './search-project-autocomplete.component';
@ -7,7 +7,7 @@ describe('SearchProjectComponent', () => {
let component: SearchProjectAutocompleteComponent; let component: SearchProjectAutocompleteComponent;
let fixture: ComponentFixture<SearchProjectAutocompleteComponent>; let fixture: ComponentFixture<SearchProjectAutocompleteComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [SearchProjectAutocompleteComponent], 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'; import { SearchRolesAutocompleteComponent } from './search-roles-autocomplete.component';
@ -8,7 +8,7 @@ describe('SearchProjectComponent', () => {
let component: SearchRolesAutocompleteComponent; let component: SearchRolesAutocompleteComponent;
let fixture: ComponentFixture<SearchRolesAutocompleteComponent>; let fixture: ComponentFixture<SearchRolesAutocompleteComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [SearchRolesAutocompleteComponent], 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'; import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
@ -6,7 +6,7 @@ describe('SearchUserAutocompleteComponent', () => {
let component: SearchUserAutocompleteComponent; let component: SearchUserAutocompleteComponent;
let fixture: ComponentFixture<SearchUserAutocompleteComponent>; let fixture: ComponentFixture<SearchUserAutocompleteComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [SearchUserAutocompleteComponent], declarations: [SearchUserAutocompleteComponent],
}) })

View File

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

View File

@ -1,6 +1,12 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()" <app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp" [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection"> [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 <button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false"> (click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i> <i class="las la-trash"></i>
@ -33,19 +39,28 @@
</ng-container> </ng-container>
<ng-container matColumnDef="user"> <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"> <td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td> {{grant?.displayName}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="org"> <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"> <td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td> {{grant.orgName}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="projectId"> <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"> <td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td> {{grant.projectName}} </td>
</ng-container> </ng-container>
@ -63,29 +78,32 @@
</ng-container> </ng-container>
<ng-container matColumnDef="roleNamesList"> <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"> <td mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"> <ng-container
<ng-container *ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
*ngIf="(grant.grantId && loadedGrantId !== grant.grantId) || (loadedProjectId !== grant.projectId)"> <div class="flex-row">
<div class="flex-row"> <div class="role">
<span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span> <span *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>
</div> </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>
<ng-container <ng-container
*ngIf="context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE"> *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-form-field class="form-field" appearance="outline">
*ngIf="loadedProjectId && loadedProjectId === grant.projectId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple <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))" [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-option>
</mat-select> </mat-select>
</mat-form-field> </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>
<ng-container <ng-container
*ngIf="context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE"> *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-form-field class="form-field" appearance="outline">
*ngIf="loadedGrantId && loadedGrantId === grant.grantId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple <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))" [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-option>
</mat-select> </mat-select>
</mat-form-field> </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>
</td> </td>
</ng-container> </ng-container>
@ -123,4 +148,11 @@
[length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)"> [length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator> </mat-paginator>
</div> </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 { .selection {
width: 50px; width: 50px;
max-width: 50px; max-width: 50px;
@ -35,15 +48,22 @@
.flex-row { .flex-row {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
max-width: 400px; max-width: 400px;
.role { .role {
display: block; display: flex;
flex-direction: column;
margin: .25rem; margin: .25rem;
font-size: 14px; font-size: 14px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
justify-items: center;
align-self: center;
}
.fill-space {
flex: 1;
} }
button { button {
@ -60,3 +80,7 @@
.fill-space { .fill-space {
flex: 1; 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'; import { UserGrantsComponent } from './user-grants.component';
@ -6,7 +6,7 @@ describe('UserGrantsComponent', () => {
let component: UserGrantsComponent; let component: UserGrantsComponent;
let fixture: ComponentFixture<UserGrantsComponent>; let fixture: ComponentFixture<UserGrantsComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [UserGrantsComponent], declarations: [UserGrantsComponent],
}) })

View File

@ -1,10 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material/input';
import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators'; 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 { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -14,12 +23,17 @@ import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource
selector: 'app-user-grants', selector: 'app-user-grants',
templateUrl: './user-grants.component.html', templateUrl: './user-grants.component.html',
styleUrls: ['./user-grants.component.scss'], styleUrls: ['./user-grants.component.scss'],
animations: [
enterAnimations,
],
}) })
export class UserGrantsComponent implements OnInit, AfterViewInit { export class UserGrantsComponent implements OnInit, AfterViewInit {
public userGrantSearchKey: UserGrantSearchKey | undefined = undefined;
public UserGrantSearchKey: any = UserGrantSearchKey;
public INITIAL_PAGE_SIZE: number = 50; public INITIAL_PAGE_SIZE: number = 50;
@Input() context: UserGrantContext = UserGrantContext.NONE; @Input() context: UserGrantContext = UserGrantContext.NONE;
@Input() refreshOnPreviousRoutes: string[] = []; @Input() refreshOnPreviousRoutes: string[] = [];
public grants: UserGrantView.AsObject[] = [];
public dataSource!: UserGrantsDataSource; public dataSource!: UserGrantsDataSource;
public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []); public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []);
@ -32,6 +46,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() userId: string = ''; @Input() userId: string = '';
@Input() projectId: string = ''; @Input() projectId: string = '';
@Input() grantId: string = ''; @Input() grantId: string = '';
@ViewChild('input') public filter!: MatInput;
public grantRoleOptions: string[] = []; public grantRoleOptions: string[] = [];
public projectRoleOptions: ProjectRoleView.AsObject[] = []; public projectRoleOptions: ProjectRoleView.AsObject[] = [];
@ -39,6 +54,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public loadedGrantId: string = ''; public loadedGrantId: string = '';
public loadedProjectId: string = ''; public loadedProjectId: string = '';
public grantToEdit: string = '';
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
@ -89,7 +105,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
.subscribe(); .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.dataSource.loadGrants(
this.context, this.context,
this.paginator?.pageIndex ?? 0, this.paginator?.pageIndex ?? 0,
@ -99,6 +124,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
grantId: this.grantId, grantId: this.grantId,
userId: this.userId, userId: this.userId,
}, },
queries,
); );
} }
@ -114,7 +140,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row)); 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.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => {
this.loadedGrantId = grantId; this.loadedGrantId = grantId;
this.grantRoleOptions = resp.toObject().roleKeysList; 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.mgmtService.SearchProjectRoles(projectId, 100, 0).then(resp => {
this.loadedProjectId = projectId; this.loadedProjectId = projectId;
this.projectRoleOptions = resp.toObject().resultList; 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 { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
@ -39,6 +40,7 @@ import { UserGrantsComponent } from './user-grants.component';
MatCheckboxModule, MatCheckboxModule,
MatTooltipModule, MatTooltipModule,
MatSelectModule, MatSelectModule,
MatInputModule,
MatFormFieldModule, MatFormFieldModule,
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,

View File

@ -11,6 +11,10 @@
.action { .action {
display: flex; display: flex;
button {
border-radius: .5rem;
}
.ok-button { .ok-button {
margin-left: .5rem; 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'; import { WarnDialogComponent } from './warn-dialog.component';
@ -6,7 +6,7 @@ describe('WarnDialogComponent', () => {
let component: WarnDialogComponent; let component: WarnDialogComponent;
let fixture: ComponentFixture<WarnDialogComponent>; let fixture: ComponentFixture<WarnDialogComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [WarnDialogComponent], declarations: [WarnDialogComponent],
}) })

View File

@ -24,6 +24,23 @@
{{'HOME.IAM'| translate}}</h2> {{'HOME.IAM'| translate}}</h2>
<p>{{'HOME.IAM_DESC'| translate}}</p> <p>{{'HOME.IAM_DESC'| translate}}</p>
</div> </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> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
<a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a> <a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a>
@ -37,6 +54,9 @@
{{'HOME.SECURITYANDPRIVACY'| translate}}</h2> {{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p> <p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
</div> </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> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
<a color="primary" mat-stroked-button <a color="primary" mat-stroked-button
@ -51,6 +71,11 @@
<i class="icon las la-layer-group"></i> <i class="icon las la-layer-group"></i>
{{'HOME.PROJECTS'| translate}}</h2> {{'HOME.PROJECTS'| translate}}</h2>
<p>{{'HOME.PROJECTS_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
@ -66,6 +91,19 @@
<h2> <i class="icon las la-archway"></i> <h2> <i class="icon las la-archway"></i>
{{'HOME.PROTECTION'| translate}}</h2> {{'HOME.PROTECTION'| translate}}</h2>
<p>{{'HOME.PROTECTION_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
@ -82,6 +120,18 @@
<i class="las la-users"></i> <i class="las la-users"></i>
{{'HOME.USERS'| translate}}</h2> {{'HOME.USERS'| translate}}</h2>
<p>{{'HOME.USERS_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">

View File

@ -32,7 +32,7 @@
justify-content: space-evenly; justify-content: space-evenly;
.item { .item {
flex: 1 1 45%; flex: 1 0 45%;
margin: 0 1rem; margin: 0 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -80,3 +80,25 @@
margin-top: 3rem; 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'; import { HomeComponent } from './home.component';
@ -6,7 +6,7 @@ describe('HomeComponent', () => {
let component: HomeComponent; let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>; let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [HomeComponent], declarations: [HomeComponent],
}).compileComponents(); }).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'; import { FailedEventsComponent } from './failed-events.component';
@ -6,7 +6,7 @@ describe('FailedEventsComponent', () => {
let component: FailedEventsComponent; let component: FailedEventsComponent;
let fixture: ComponentFixture<FailedEventsComponent>; let fixture: ComponentFixture<FailedEventsComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [FailedEventsComponent], 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 { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('IamMembersComponent', () => {
let component: IamMembersComponent; let component: IamMembersComponent;
let fixture: ComponentFixture<IamMembersComponent>; let fixture: ComponentFixture<IamMembersComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IamMembersComponent], declarations: [IamMembersComponent],
imports: [ 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'; import { IamViewsComponent } from './iam-views.component';
@ -6,7 +6,7 @@ describe('IamViewsComponent', () => {
let component: IamViewsComponent; let component: IamViewsComponent;
let fixture: ComponentFixture<IamViewsComponent>; let fixture: ComponentFixture<IamViewsComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IamViewsComponent], 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'; import { IamComponent } from './iam.component';
@ -6,7 +6,7 @@ describe('IamComponent', () => {
let component: IamComponent; let component: IamComponent;
let fixture: ComponentFixture<IamComponent>; let fixture: ComponentFixture<IamComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IamComponent], 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'; import { OrgCreateComponent } from './org-create.component';
@ -6,7 +6,7 @@ describe('OrgCreateComponent', () => {
let component: OrgCreateComponent; let component: OrgCreateComponent;
let fixture: ComponentFixture<OrgCreateComponent>; let fixture: ComponentFixture<OrgCreateComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgCreateComponent], 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'; import { AddDomainDialogComponent } from './add-domain-dialog.component';
@ -6,7 +6,7 @@ describe('WarnDialogComponent', () => {
let component: AddDomainDialogComponent; let component: AddDomainDialogComponent;
let fixture: ComponentFixture<AddDomainDialogComponent>; let fixture: ComponentFixture<AddDomainDialogComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AddDomainDialogComponent], declarations: [AddDomainDialogComponent],
}) })

View File

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

View File

@ -1,10 +1,10 @@
.btn-container { .btn-container {
display: flex; display: flex;
margin: -.5rem; margin: -.5rem;
align-items: center;
button { button {
margin: 1rem .5rem; 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'; import { DomainVerificationComponent } from './domain-verification.component';
@ -6,7 +6,7 @@ describe('DomainVerificationComponent', () => {
let component: DomainVerificationComponent; let component: DomainVerificationComponent;
let fixture: ComponentFixture<DomainVerificationComponent>; let fixture: ComponentFixture<DomainVerificationComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DomainVerificationComponent], declarations: [DomainVerificationComponent],
}) })

View File

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

View File

@ -36,7 +36,7 @@
.verified, .verified,
.primary { .primary {
color: #5282c1; color: var(--color-main);
margin-right: 1rem; 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'; import { OrgDetailComponent } from './org-detail.component';
@ -6,7 +6,7 @@ describe('OrgDetailComponent', () => {
let component: OrgDetailComponent; let component: OrgDetailComponent;
let fixture: ComponentFixture<OrgDetailComponent>; let fixture: ComponentFixture<OrgDetailComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgDetailComponent], declarations: [OrgDetailComponent],
}) })

View File

@ -79,10 +79,12 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
this.loadMembers(); 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.domains = result.toObject().resultList;
this.primaryDomain = this.domains.find(domain => domain.primary)?.domain ?? ''; 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 { public setPrimary(domain: OrgDomainView.AsObject): void {
this.mgmtService.setMyPrimaryOrgDomain(domain.domain).then(() => { this.mgmtService.setMyPrimaryOrgDomain(domain.domain).then(() => {
this.toast.showInfo('ORG.TOAST.SETPRIMARY', true); this.toast.showInfo('ORG.TOAST.SETPRIMARY', true);
this.getData(); this.loadDomains();
}).catch((error) => { }).catch((error) => {
this.toast.showError(error); this.toast.showError(error);
}); });
@ -202,12 +204,18 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
} }
public verifyDomain(domain: OrgDomainView.AsObject): void { public verifyDomain(domain: OrgDomainView.AsObject): void {
this.dialog.open(DomainVerificationComponent, { const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: { data: {
domain: domain, domain: domain,
}, },
width: '500px', width: '500px',
}); });
dialogRef.afterClosed().subscribe((reload) => {
if (reload) {
this.loadDomains();
}
});
} }
public loadMembers(): void { 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'; import { OrgListComponent } from './org-list.component';
@ -6,7 +6,7 @@ describe('OrgListComponent', () => {
let component: OrgListComponent; let component: OrgListComponent;
let fixture: ComponentFixture<OrgListComponent>; let fixture: ComponentFixture<OrgListComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgListComponent], 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'; import { OrgMemberRolesAutocompleteComponent } from './org-member-roles-autocomplete.component';
@ -6,7 +6,7 @@ describe('OrgMemberRolesAutocompleteComponent', () => {
let component: OrgMemberRolesAutocompleteComponent; let component: OrgMemberRolesAutocompleteComponent;
let fixture: ComponentFixture<OrgMemberRolesAutocompleteComponent>; let fixture: ComponentFixture<OrgMemberRolesAutocompleteComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgMemberRolesAutocompleteComponent], 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 { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
@ -10,7 +10,7 @@ describe('OrgMembersComponent', () => {
let component: OrgMembersComponent; let component: OrgMembersComponent;
let fixture: ComponentFixture<OrgMembersComponent>; let fixture: ComponentFixture<OrgMembersComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OrgMembersComponent], declarations: [OrgMembersComponent],
imports: [ imports: [

View File

@ -8,6 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@ -51,6 +52,7 @@ import { OrgsRoutingModule } from './orgs-routing.module';
MemberCreateDialogModule, MemberCreateDialogModule,
MatMenuModule, MatMenuModule,
ChangesModule, ChangesModule,
MatProgressSpinnerModule,
AddDomainDialogModule, AddDomainDialogModule,
TranslateModule, TranslateModule,
SharedModule, 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'; import { AppCreateComponent } from './app-create.component';
@ -6,7 +6,7 @@ describe('AppCreateComponent', () => {
let component: AppCreateComponent; let component: AppCreateComponent;
let fixture: ComponentFixture<AppCreateComponent>; let fixture: ComponentFixture<AppCreateComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AppCreateComponent], 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'; import { AppDetailComponent } from './app-detail.component';
@ -6,7 +6,7 @@ describe('AppDetailComponent', () => {
let component: AppDetailComponent; let component: AppDetailComponent;
let fixture: ComponentFixture<AppDetailComponent>; let fixture: ComponentFixture<AppDetailComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AppDetailComponent], 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'; import { AppSecretDialogComponent } from './app-secret-dialog.component';
@ -6,7 +6,7 @@ describe('AppSecretDialogComponent', () => {
let component: AppSecretDialogComponent; let component: AppSecretDialogComponent;
let fixture: ComponentFixture<AppSecretDialogComponent>; let fixture: ComponentFixture<AppSecretDialogComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AppSecretDialogComponent], 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'; import { GrantedProjectDetailComponent } from './granted-project-detail.component';
@ -6,7 +6,7 @@ describe('GrantedProjectDetailComponent', () => {
let component: GrantedProjectDetailComponent; let component: GrantedProjectDetailComponent;
let fixture: ComponentFixture<GrantedProjectDetailComponent>; let fixture: ComponentFixture<GrantedProjectDetailComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [GrantedProjectDetailComponent], declarations: [GrantedProjectDetailComponent],
}) })

View File

@ -22,11 +22,10 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
<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> </div>
</div> </div>
@ -47,12 +46,20 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button" <button [ngClass]="{ selected: selection.isSelected(item)}"
mat-icon-button> (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 *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon> <mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button> </button>
</div> </div>
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0"> <p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p> {{'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'; import { GrantedProjectGridComponent } from './granted-project-grid.component';
@ -6,7 +6,7 @@ describe('GridComponent', () => {
let component: GrantedProjectGridComponent; let component: GrantedProjectGridComponent;
let fixture: ComponentFixture<GrantedProjectGridComponent>; let fixture: ComponentFixture<GrantedProjectGridComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [GrantedProjectGridComponent], declarations: [GrantedProjectGridComponent],
}) })

View File

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