resolve conflicts

This commit is contained in:
Elio Bischof 2022-08-24 10:14:05 +02:00
commit d6a05b3a61
No known key found for this signature in database
GPG Key ID: 7B383FDE4DDBF1BD
611 changed files with 34386 additions and 38671 deletions

3
.artifacts/zitadel/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@ -2,7 +2,7 @@
name: "\U0001F41B Bug report"
about: Create a report to help us improve
title: ''
labels: bug
labels: 'state: triage, type: bug'
assignees: ''
---

View File

@ -1,13 +0,0 @@
---
name: OKR
about: The objective key result is an overarching goal that we are working towards.
title: ''
labels: OKR
assignees: ''
---
OKR description
- [ ] Link to subordinate story
- [ ] Link to subordinate story

View File

@ -1,14 +0,0 @@
---
name: Story
about: A story describes the basic objective and contains several tasks that are to
be implemented
title: ''
labels: story
assignees: ''
---
As a [type of user], I want [a goal or objective], so that [customer benefit or value].
- [ ] Link to subordinate task
- [ ] Link to subordinate task

View File

@ -3,16 +3,13 @@ name: Task
about: A task describes what is to be implemented and which acceptance criteria must
be met.
title: ''
labels: task
labels: 'state: triage'
assignees: ''
---
Description
- [ ] Todo
- [ ] Todo
**Acceptance criteria**
- [ ] ...
- [ ] ...

49
.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: "ZITADEL e2e Tests"
on:
workflow_run:
workflows: [ZITADEL Release]
types:
- completed
workflow_dispatch:
inputs:
releaseversion:
description: 'Release version to test'
required: true
default: 'latest'
jobs:
test:
strategy:
matrix:
browser: [firefox, chrome]
runs-on: ubuntu-20.04
env:
ZITADEL_IMAGE_REGISTRY: 'ghcr.io/zitadel/zitadel'
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set TAG env manual trigger
if: github.event_name == 'workflow_dispatch'
run: echo "ZITADEL_IMAGE=${ZITADEL_IMAGE_REGISTRY}:${{ github.event.inputs.releaseversion }}" >> $GITHUB_ENV
- name: Set TAG env on ZITADEL release
if: github.event_name == 'workflow_run'
run: echo "ZITADEL_IMAGE=${ZITADEL_IMAGE_REGISTRY}:${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test ${{ matrix.browser }}
run: docker compose run e2e --browser ${{ matrix.browser }}
working-directory: e2e
- name: Archive production tests ${{ matrix.browser }}
if: always()
uses: actions/upload-artifact@v2
with:
name: production-tests-${{ matrix.browser }}
path: |
e2e/cypress/results
e2e/cypress/videos
e2e/cypress/screenshots
retention-days: 30

52
.github/workflows/release-channels.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: ZITADEL Release tags
on:
push:
branches:
- "main"
paths:
- 'release-channels.yaml'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
Build:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: get stable tag
run: echo STABLE_RELEASE=$(yq eval '.stable' release-channels.yaml) >> $GITHUB_ENV
- name: checkout stable tag
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ env.STABLE_RELEASE }}
- name: GitHub Container Registry Login
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Google Artifact Registry Login
uses: docker/login-action@v1
with:
registry: europe-docker.pkg.dev
username: _json_key_base64
password: ${{ secrets.GCR_JSON_KEY_BASE64 }}
- name: copy release to stable
run: |
skopeo --version
skopeo copy --all docker://ghcr.io/zitadel/zitadel:$STABLE_RELEASE docker://ghcr.io/zitadel/zitadel:stable

54
.github/workflows/test-code.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: ZITADEL PR
on:
pull_request:
paths-ignore:
- 'docs/**'
- 'guides/**'
- '**.md'
jobs:
Test:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
install-only: true
version: v1.8.3
- name: Build and Unit Test
run: GOOS="linux" GOARCH="amd64" goreleaser build --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel
- name: Publish go coverage
uses: codecov/codecov-action@v3.1.0
with:
file: .artifacts/codecov/profile.cov
name: go-codecov
# As goreleaser doesn't build a dockerfile in snapshot mode, we have to build it here
- name: Build Docker Image
run: docker build -t zitadel:pr --file build/Dockerfile .artifacts/zitadel
- name: Run E2E Tests
run: docker compose run e2e
working-directory: e2e
env:
ZITADEL_IMAGE: zitadel:pr
- name: Archive Test Results
if: always()
uses: actions/upload-artifact@v2
with:
name: pull-request-tests
path: |
e2e/cypress/results
e2e/cypress/videos
e2e/cypress/screenshots
retention-days: 30

20
.github/workflows/test-docs.yml vendored Normal file
View File

@ -0,0 +1,20 @@
# ATTENTION: Although this workflow doesn't do much, it is still important.
# It is complementary to the workflow in the file test-code.yml.
# It enables to exclude files for the workflow and still mark the Test job as required without having pending PRs.
# GitHub recommends this solution here:
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: ZITADEL PR
on:
pull_request:
paths:
- 'docs/**'
- 'guides/**'
- '**.md'
jobs:
Test:
runs-on: ubuntu-20.04
steps:
- run: 'echo "No tests for docs are implemented, yet"'

View File

@ -1,45 +0,0 @@
name: ZITADEL PR
on:
pull_request:
jobs:
Go:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test
run: docker build -f build/grpc/Dockerfile -t zitadel-base:local . && docker build -f build/zitadel/Dockerfile . -t zitadel-go-test --target go-codecov -o .artifacts/codecov
- name: Publish go coverage
uses: codecov/codecov-action@v3.1.0
with:
file: .artifacts/codecov/profile.cov
name: go-codecov
Angular:
runs-on: ubuntu-20.04
env:
DOCKER_BUILDKIT: 1
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: docker
install: true
- name: Test
run: docker build -f build/grpc/Dockerfile -t zitadel-base:local . && docker build -f build/console/Dockerfile . -t zitadel-npm-base --target angular-build

3
.gitignore vendored
View File

@ -61,6 +61,7 @@ openapi/**/*.json
build/local/*.env
migrations/cockroach/migrate_cloud.go
.notifications
.artifacts
/.artifacts/*
!/.artifacts/zitadel
/zitadel

View File

@ -5,16 +5,22 @@ release:
owner: zitadel
name: zitadel
draft: false
# If set to auto, will mark the release as not ready for production
# in case there is an indicator for this in the tag e.g. v1.0.0-rc1
# If set to true, will mark the release as not ready for production.
# Default is false.
prerelease: auto
before:
hooks:
# this file would invalidate go source caches
- sh -c "rm openapi/statik/statik.go || true"
- docker build -f build/grpc/Dockerfile -t zitadel-base:local .
- docker build -f build/zitadel/Dockerfile . -t zitadel-go-test --target go-codecov -o .artifacts/codecov
- docker build -f build/zitadel/Dockerfile . -t zitadel-go-base --target go-copy -o .artifacts/grpc/go-client
- sh -c "cp -r .artifacts/grpc/go-client/* ."
- docker build -f build/console/Dockerfile . -t zitadel-npm-base --target npm-copy -o .artifacts/grpc/js-client
- docker build -f build/console/Dockerfile . -t zitadel-npm-base --target angular-export -o .artifacts/console
- docker build -f build/console/Dockerfile . -t zitadel-npm-console --target angular-export -o .artifacts/console
- sh -c "cp -r .artifacts/console/* internal/api/ui/console/static/"
builds:
@ -35,9 +41,8 @@ dist: .artifacts/goreleaser
dockers:
- image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .ShortCommit }}-amd64
- europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:{{ .Tag }}-amd64
- europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:{{ .ShortCommit }}-amd64
goarch: amd64
use: buildx
dockerfile: build/Dockerfile
build_flag_templates:
@ -45,11 +50,27 @@ dockers:
- image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
- ghcr.io/zitadel/zitadel:{{ .ShortCommit }}-arm64
goarch: arm64
use: buildx
dockerfile: build/Dockerfile
build_flag_templates:
- "--platform=linux/arm64"
docker_manifests:
- id: zitadel-latest
name_template: ghcr.io/zitadel/zitadel:latest
image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
# Skips can and shall be set for individual manifests same as in dockers
skip_push: auto
- id: zitadel-Tag
name_template: ghcr.io/zitadel/zitadel:{{ .Tag }}
image_templates:
- ghcr.io/zitadel/zitadel:{{ .Tag }}-amd64
- ghcr.io/zitadel/zitadel:{{ .Tag }}-arm64
archives:
- name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
replacements:
@ -96,6 +117,10 @@ brews:
- name: git
install: |-
bin.install "zitadel"
# If set to auto, the release will not be uploaded to the homebrew tap
# in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
# Default is false.
skip_upload: auto
announce:
discord:

View File

@ -1,9 +1,7 @@
module.exports = {
branches: [
{name: 'main'},
{name: '1.x.x', range: '1.x.x', channel: '1.x.x'},
{name: 'v2-alpha', prerelease: true},
{name: 'update-projection-on-query', prerelease: true},
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
],
plugins: [
"@semantic-release/commit-analyzer"

View File

@ -38,12 +38,10 @@ We accept contributions through pull requests. You need a github account for tha
### Submit a Pull Request (PR)
> :warning: Currently main development is done on branch `v2-alpha`. Make sure you're merging into the correct branch.
1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [zitadel/zitadel](https://github.com/zitadel/zitadel) repository on GitHub
2. On your fork, commit your changes to a new branch
`git checkout -b my-fix-branch v2-alpha`
`git checkout -b my-fix-branch main`
3. Make your changes following the [guidelines](#contribute) in this guide. Make sure that all tests pass.
@ -51,7 +49,7 @@ We accept contributions through pull requests. You need a github account for tha
`git commit --all`
5. [Merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) the latest commit of the `v2-alpha`-branch
5. [Merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) the latest commit of the `main`-branch
6. Push the changes to your branch on Github
@ -59,7 +57,7 @@ We accept contributions through pull requests. You need a github account for tha
7. Use [Semantic Release commit messages](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type) to simplify creation of release notes. In the title of the pull request [correct tagging](#commit-messages) is required and will be requested by the reviewers.
8. On GitHub, [send a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to `zitadel:v2-alpha`. Request review from one of the maintainers.
8. On GitHub, [send a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to `zitadel:main`. Request review from one of the maintainers.
### Reviewing a Pull Request
@ -124,7 +122,7 @@ Make sure to use the following configurations:
To run console locally, navigate to the console subfolder and run `npm install` and then `npm start` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
Console loads its environment from the [environment.json](https://github.com/zitadel/zitadel/blob/v2-alpha/console/src/assets/environment.json), make sure to change the configuration to your instance project.
Console loads its environment from the [environment.json](https://github.com/zitadel/zitadel/blob/main/console/src/assets/environment.json), make sure to change the configuration to your instance project.
When the backend is running locally ensure you are specifying your [localhost](http://localhost:8080/ui/console/assets/environment.json) endpoints.
### API Definitions

View File

@ -1,3 +1,14 @@
<p align="right">
<img src="./docs/static/img/github-header01-dark@2x.png#gh-dark-mode-only" alt="ZITADEL Cloud launched" max-height="200px" width="auto" />
<img src="./docs/static/img/github-header01-light@2x.png#gh-light-mode-only" alt="ZITADEL Cloud launched" max-height="200px" width="auto" />
</p>
<p align="center">
We are live on <a href="https://www.producthunt.com/posts/zitadel">ProductHunt</a>. <br> Thank you for the support and feedback.
</p>
---
<p align="center">
<img src="./docs/static/logos/zitadel-logo-dark@2x.png#gh-light-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" />
<img src="./docs/static/logos/zitadel-logo-light@2x.png#gh-dark-mode-only" alt="Zitadel Logo" max-height="200px" width="auto" />
@ -41,30 +52,30 @@ With ZITADEL you rely on a battle tested, hardened and extensible turnkey soluti
## Get started
### ZITADEL Cloud
### ZITADEL Cloud (SaaS)
The easiest way to get started with ZITADEL is to use our public cloud offering.
Currently ZITADEL V2 Beta is available, head over to [https://zitadel.cloud](https://zitadel.cloud) and create your first ZITADEL instance for free.
You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing/v2).
You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing).
### Install ZITADEL
- [We provide installation guides for multiple platforms here](https://docs.zitadel.com/docs/guides/installation)
- [We provide installation guides for multiple platforms here](https://docs.zitadel.com/docs/guides/deploy/overview)
### Quickstarts - Integrate your app
- [Multiple Quickstarts can be found here](https://docs.zitadel.com/docs/quickstarts/introduction)
- [Multiple Quickstarts can be found here](https://docs.zitadel.com/docs/guides/start/quickstart)
- [And even more examples are located under zitadel/zitadel-examples](https://github.com/zitadel/zitadel-examples)
> If you miss something please feel free to engage with us [here](https://github.com/zitadel/zitadel/discussions/1717)
> If you miss something please feel free to [join the Discussion](https://github.com/zitadel/zitadel/discussions/1717)
## Why ZITADEL
- [API-first](https://docs.zitadel.com/docs/apis/introduction)
- Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore)
- Strong audit trail thanks to [event sourcing](https://docs.zitadel.com/docs/concepts/eventstore/overview)
- [Actions](https://docs.zitadel.ch/docs/concepts/features/actions) to react on events with custom code
- [Branding](https://docs.zitadel.com/docs/guides/customization/branding) for a uniform user experience
- [Branding](https://docs.zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience
- [CockroachDB](https://www.cockroachlabs.com/) is the only dependency
## Features
@ -73,16 +84,17 @@ You can also discover our pay-as-you-go [pricing](https://zitadel.com/pricing/v2
- Passwordless with FIDO2 support
- Username / Password
- Multifactor authentication with OTP, U2F
- [Identity Brokering](https://docs.zitadel.com/docs/guides/authentication/identity-brokering)
- [Machine-to-machine (JWT profile)](https://docs.zitadel.com/docs/guides/authentication/serviceusers)
- [Identity Brokering](https://docs.zitadel.com/docs/guides/integrate/identity-brokering)
- [Machine-to-machine (JWT profile)](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
- Personal Access Tokens (PAT)
- Role Based Access Control (RBAC)
- [Delegate role management to third-parties](https://docs.zitadel.com/docs/guides/basics/projects#what-is-a-granted-project)
- [Delegate role management to third-parties](https://docs.zitadel.com/docs/guides/manage/console/projects)
- Self-registration including verification
- User self service
- [Service Accounts](https://docs.zitadel.com/docs/guides/authentication/serviceusers)
- [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
- [OpenID Connect certified](https://openid.net/certification/#OPs)
- 🚧 [SAML 2.0](https://github.com/zitadel/zitadel/pull/3618)
- 🚧 [Postgres](https://github.com/zitadel/zitadel/pull/3998)
## Client libraries
@ -125,7 +137,7 @@ Use our login widget to allow easy and secure access to your applications and en
- works on all modern platforms, devices, and browsers
- phishing resistant alternative
- requires only one gesture by the user
- easy [enrollment](https://docs.zitadel.com/docs/manuals/user-factors) of the device during registration
- easy [enrollment](https://docs.zitadel.com/docs/manuals/user-profile) of the device during registration
![passwordless-windows-hello](https://user-images.githubusercontent.com/1366906/118765435-5d419780-b87b-11eb-95bf-55140119c0d8.gif)
@ -142,16 +154,6 @@ Delegate the right to assign roles to another organization
Customize login and console with your design
![private_labeling](https://user-images.githubusercontent.com/1366906/123089110-d148ff80-d426-11eb-9598-32b506f6d4fd.gif)
## Usage Data
ZITADEL components send errors and usage data to CAOS Ltd., so that we are able to identify code improvement potential. If you don't want to send this data or don't have an internet connection, pass the global flag `--disable-analytics` when using zitadelctl. For disabling ingestion for already-running components, execute the takeoff command again with the `` flag.
We try to distinguishing the environments from which events come from. As environment identifier, we enrich the events by the domain you have configured in zitadel.yml, as soon as it's available. When it's not available and you passed the --gitops flag, we defer the environment identifier from your git repository URL.
Besides from errors that don't clearly come from misconfiguration or cli misusage, we send an initial event when any binary is started. This is a "<component> invoked" event along with the flags that are passed to it, except secret values of course.
We only ingest operational data. Your ZITADEL workload data from the IAM application itself is never sent anywhere unless you chose to integrate other systems yourself.
## Security
See the policy [here](./SECURITY.md)
@ -161,3 +163,4 @@ See the policy [here](./SECURITY.md)
See the exact licensing terms [here](./LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -3,10 +3,16 @@ ARG NODE_VERSION=16
#######################
## With this step we prepare all node_modules, this helps caching the build
## Speed up this step by mounting your local node_modules directory
## We also copy and generate the source code
#######################
FROM node:${NODE_VERSION} as npm-base
WORKDIR /console
# Dependencies
COPY console/package.json console/package-lock.json ./
RUN npm ci
# Sources
COPY console .
COPY --from=zitadel-base:local /proto /proto
COPY --from=zitadel-base:local /usr/local/bin /usr/local/bin/.
@ -24,12 +30,8 @@ COPY --from=npm-base /console/src/app/proto/generated /console/src/app/proto/gen
#######################
FROM npm-base as angular-build
COPY console/package.json console/package-lock.json ./
RUN npm ci
RUN npm run lint
RUN npm run prodbuild
RUN ls -la /console/dist/console
#######################
## Only Copy Assets

View File

@ -5,18 +5,17 @@ ARG GO_VERSION=1.17
## Speed up this step by mounting your local go mod pkg directory
#######################
FROM golang:${GO_VERSION} as go-dep
RUN mkdir -p src/github.com/zitadel/zitadel
WORKDIR /go/src/github.com/zitadel/zitadel
#download modules
COPY . .
COPY go.mod ./
COPY go.sum ./
RUN go mod download
# install tools
COPY tools ./tools
RUN ./tools/install.sh
#######################
## generates static files
#######################
@ -47,17 +46,20 @@ COPY internal/api/assets/generator internal/api/assets/generator
COPY internal/config internal/config
COPY internal/errors internal/errors
RUN build/zitadel/generate-grpc.sh \
&& go generate openapi/statik/generate.go \
&& mkdir -p docs/apis/assets/ \
&& go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=docs/apis/assets/assets.md
RUN build/zitadel/generate-grpc.sh && \
go generate openapi/statik/generate.go && \
mkdir -p docs/apis/assets/ && \
go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=docs/apis/assets/assets.md
#######################
## Go base build
#######################
FROM go-stub as go-base
# copy remaining zitadel files
COPY . .
COPY cmd cmd
COPY internal internal
COPY pkg pkg
COPY openapi openapi
#######################
## copy for local dev

View File

@ -182,6 +182,9 @@ protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,settings.md \
${PROTO_PATH}/settings.proto
protoc \
-I=/proto/include \
--doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,v1.md \
${PROTO_PATH}/v1.proto
echo "done generating grpc"

View File

@ -3,6 +3,11 @@ Log:
Formatter:
Format: text
# Exposes metrics on /debug/metrics
Metrics:
# Select type otel (OpenTelemetry) or none (disables collection and endpoint)
Type: otel
# Port ZITADEL will listen on
Port: 8080
# Port ZITADEL is exposed on, it can differ from port e.g. if you proxy the traffic
@ -40,6 +45,7 @@ HTTP1HostHeader: "host"
WebAuthNName: ZITADEL
Database:
cockroach:
Host: localhost
Port: 26257
Database: zitadel
@ -55,8 +61,7 @@ Database:
RootCert: ""
Cert: ""
Key: ""
AdminUser:
Admin:
Username: root
Password: ""
SSL:
@ -98,10 +103,20 @@ Machine:
# Url: "http://169.254.169.254/metadata/instance?api-version=2021-02-01"
# JPath: "$.compute.vmId"
# Storage for assets like user avatar, organization logo, icon, font, ...
AssetStorage:
Type: db
# HTTP cache control settings for serving assets in the assets API and login UI
# the assets will also be served with an etag and last-modified header
Cache:
MaxAge: 5s
SharedMaxAge: 168h #7d
Projections:
RequeueEvery: 10s
RequeueEvery: 60s
RetryFailedAfter: 1s
MaxFailureCount: 5
ConcurrentInstances: 10
BulkLimit: 200
MaxIterators: 1
Customizations:
@ -112,6 +127,7 @@ Auth:
SearchLimit: 1000
Spooler:
ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000
FailureCountUntilSkip: 5
@ -119,6 +135,7 @@ Admin:
SearchLimit: 1000
Spooler:
ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000
FailureCountUntilSkip: 5
@ -169,12 +186,13 @@ Console:
SharedMaxAge: 5m
LongCache:
MaxAge: 12h
SharedMaxAge: 168h
SharedMaxAge: 168h #7d
Notification:
Repository:
Spooler:
ConcurrentWorkers: 1
ConcurrentInstances: 10
BulkLimit: 10000
FailureCountUntilSkip: 5
Handlers:
@ -206,10 +224,11 @@ EncryptionKeys:
SystemAPIUsers:
# add keys for authentication of the systemAPI here:
# you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT:
# - superuser:
# Path: /path/to/superuser/key.pem
# Path: /path/to/superuser/key.pem # you can provide the key either by reference with the path
# - superuser2:
# Path: /path/to/superuser2/key.pem
# KeyData: <base64 encoded key> # or you can directly embed it as base64 encoded value
#TODO: remove as soon as possible
SystemDefaults:
@ -686,6 +705,17 @@ InternalAuthZ:
- "project.grant.read"
- "project.grant.member.read"
- "project.grant.user.grant.read"
- Role: "ORG_SETTINGS_MANAGER"
Permissions:
- "org.read"
- "org.write"
- "org.member.read"
- "org.idp.read"
- "org.idp.write"
- "org.idp.delete"
- "policy.read"
- "policy.write"
- "policy.delete"
- Role: "ORG_USER_PERMISSION_EDITOR"
Permissions:
- "org.read"

View File

@ -10,14 +10,15 @@ import (
type Config struct {
Database database.Config
AdminUser database.User
Machine *id.Config
Log *logging.Config
}
func MustNewConfig(v *viper.Viper) *Config {
config := new(Config)
err := v.Unmarshal(config)
err := v.Unmarshal(config,
viper.DecodeHook(database.DecodeHook),
)
logging.OnError(err).Fatal("unable to read config")
err = config.Log.SetLogger()
@ -25,21 +26,3 @@ func MustNewConfig(v *viper.Viper) *Config {
return config
}
func adminConfig(config *Config) database.Config {
adminConfig := config.Database
adminConfig.Username = config.AdminUser.Username
adminConfig.Password = config.AdminUser.Password
adminConfig.SSL.Cert = config.AdminUser.SSL.Cert
adminConfig.SSL.Key = config.AdminUser.SSL.Key
if config.AdminUser.SSL.RootCert != "" {
adminConfig.SSL.RootCert = config.AdminUser.SSL.RootCert
}
if config.AdminUser.SSL.Mode != "" {
adminConfig.SSL.Mode = config.AdminUser.SSL.Mode
}
//use default database because the zitadel database might not exist
adminConfig.Database = ""
return adminConfig
}

View File

@ -39,10 +39,10 @@ The user provided by flags needs privileges to
func InitAll(config *Config) {
id.Configure(config.Machine)
err := initialise(config,
VerifyUser(config.Database.Username, config.Database.Password),
VerifyDatabase(config.Database.Database),
VerifyGrant(config.Database.Database, config.Database.Username),
err := initialise(config.Database,
VerifyUser(config.Database.Username(), config.Database.Password()),
VerifyDatabase(config.Database.Database()),
VerifyGrant(config.Database.Database(), config.Database.Username()),
)
logging.OnError(err).Fatal("unable to initialize the database")
@ -50,10 +50,10 @@ func InitAll(config *Config) {
logging.OnError(err).Fatal("unable to initialize ZITADEL")
}
func initialise(config *Config, steps ...func(*sql.DB) error) error {
func initialise(config database.Config, steps ...func(*sql.DB) error) error {
logging.Info("initialization started")
db, err := database.Connect(adminConfig(config))
db, err := database.Connect(config, true)
if err != nil {
return err
}

View File

@ -34,7 +34,7 @@ The user provided by flags needs priviledge to
Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New())
err := initialise(config, VerifyDatabase(config.Database.Database))
err := initialise(config.Database, VerifyDatabase(config.Database.Database()))
logging.OnError(err).Fatal("unable to initialize the database")
},
}

View File

@ -28,7 +28,7 @@ Prereqesits:
Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New())
err := initialise(config, VerifyGrant(config.Database.Database, config.Database.Username))
err := initialise(config.Database, VerifyGrant(config.Database.Database(), config.Database.Username()))
logging.OnError(err).Fatal("unable to set grant")
},
}

View File

@ -33,7 +33,7 @@ The user provided by flags needs priviledge to
Run: func(cmd *cobra.Command, args []string) {
config := MustNewConfig(viper.New())
err := initialise(config, VerifyUser(config.Database.Username, config.Database.Password))
err := initialise(config.Database, VerifyUser(config.Database.Username(), config.Database.Password()))
logging.OnError(err).Fatal("unable to init user")
},
}

View File

@ -95,7 +95,7 @@ func VerifyZitadel(db *sql.DB) error {
func verifyZitadel(config database.Config) error {
logging.WithFields("database", config.Database).Info("verify zitadel")
db, err := database.Connect(config)
db, err := database.Connect(config, false)
if err != nil {
return err
}

View File

@ -124,7 +124,7 @@ func openFile(fileName string) (io.Reader, error) {
}
func keyStorage(config database.Config, masterKey string) (crypto.KeyStorage, error) {
db, err := database.Connect(config)
db, err := database.Connect(config, false)
if err != nil {
return nil, err
}

View File

@ -16,9 +16,8 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
)
type DefaultInstance struct {
type FirstInstance struct {
InstanceName string
CustomDomain string
DefaultLanguage language.Tag
Org command.OrgSetup
@ -33,9 +32,10 @@ type DefaultInstance struct {
externalDomain string
externalSecure bool
externalPort uint16
domain string
}
func (mig *DefaultInstance) Execute(ctx context.Context) error {
func (mig *FirstInstance) Execute(ctx context.Context) error {
keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey)
if err != nil {
return fmt.Errorf("cannot start key storage: %w", err)
@ -77,7 +77,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
}
mig.instanceSetup.InstanceName = mig.InstanceName
mig.instanceSetup.CustomDomain = mig.CustomDomain
mig.instanceSetup.CustomDomain = mig.externalDomain
mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage
mig.instanceSetup.Org = mig.Org
mig.instanceSetup.Org.Human.Email.Address = strings.TrimSpace(mig.instanceSetup.Org.Human.Email.Address)
@ -89,7 +89,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
return err
}
func (mig *DefaultInstance) String() string {
func (mig *FirstInstance) String() string {
return "03_default_instance"
}

View File

@ -36,6 +36,7 @@ func MustNewConfig(v *viper.Viper) *Config {
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
)),
)
logging.OnError(err).Fatal("unable to read default config")
@ -49,7 +50,7 @@ func MustNewConfig(v *viper.Viper) *Config {
type Steps struct {
s1ProjectionTable *ProjectionTable
s2AssetsTable *AssetTable
S3DefaultInstance *DefaultInstance
FirstInstance *FirstInstance
}
type encryptionKeyConfig struct {

View File

@ -0,0 +1,60 @@
package setup
import (
"context"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/eventstore"
)
type externalConfigChange struct {
es *eventstore.Eventstore
ExternalDomain string `json:"externalDomain"`
ExternalSecure bool `json:"externalSecure"`
ExternalPort uint16 `json:"externalPort"`
currentExternalDomain string
currentExternalSecure bool
currentExternalPort uint16
}
func (mig *externalConfigChange) SetLastExecution(lastRun map[string]interface{}) {
mig.currentExternalDomain, _ = lastRun["externalDomain"].(string)
externalPort, _ := lastRun["externalPort"].(float64)
mig.currentExternalPort = uint16(externalPort)
mig.currentExternalSecure, _ = lastRun["externalSecure"].(bool)
}
func (mig *externalConfigChange) Check() bool {
return mig.currentExternalSecure != mig.ExternalSecure ||
mig.currentExternalPort != mig.ExternalPort ||
mig.currentExternalDomain != mig.ExternalDomain
}
func (mig *externalConfigChange) Execute(ctx context.Context) error {
cmd, err := command.StartCommands(mig.es,
systemdefaults.SystemDefaults{},
nil,
nil,
nil,
mig.ExternalDomain,
mig.ExternalSecure,
mig.ExternalPort,
nil,
nil,
nil,
nil,
nil,
nil,
nil)
if err != nil {
return err
}
return cmd.ChangeSystemConfig(ctx, mig.currentExternalDomain, mig.currentExternalPort, mig.currentExternalSecure)
}
func (mig *externalConfigChange) String() string {
return "config_change"
}

View File

@ -54,7 +54,9 @@ func Flags(cmd *cobra.Command) {
}
func Setup(config *Config, steps *Steps, masterKey string) {
dbClient, err := database.Connect(config.Database)
logging.Info("setup started")
dbClient, err := database.Connect(config.Database, false)
logging.OnError(err).Fatal("unable to connect to database")
eventstoreClient, err := eventstore.Start(dbClient)
@ -64,33 +66,37 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient}
steps.s2AssetsTable = &AssetTable{dbClient: dbClient}
steps.S3DefaultInstance.instanceSetup = config.DefaultInstance
steps.S3DefaultInstance.userEncryptionKey = config.EncryptionKeys.User
steps.S3DefaultInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
steps.S3DefaultInstance.masterKey = masterKey
steps.S3DefaultInstance.db = dbClient
steps.S3DefaultInstance.es = eventstoreClient
steps.S3DefaultInstance.defaults = config.SystemDefaults
steps.S3DefaultInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings
steps.S3DefaultInstance.externalDomain = config.ExternalDomain
steps.S3DefaultInstance.externalSecure = config.ExternalSecure
steps.S3DefaultInstance.externalPort = config.ExternalPort
steps.FirstInstance.instanceSetup = config.DefaultInstance
steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User
steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
steps.FirstInstance.masterKey = masterKey
steps.FirstInstance.db = dbClient
steps.FirstInstance.es = eventstoreClient
steps.FirstInstance.defaults = config.SystemDefaults
steps.FirstInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings
steps.FirstInstance.externalDomain = config.ExternalDomain
steps.FirstInstance.externalSecure = config.ExternalSecure
steps.FirstInstance.externalPort = config.ExternalPort
repeatableSteps := []migration.RepeatableMigration{
&externalConfigChange{
es: eventstoreClient,
ExternalDomain: config.ExternalDomain,
ExternalPort: config.ExternalPort,
ExternalSecure: config.ExternalSecure,
},
}
ctx := context.Background()
err = migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable)
logging.OnError(err).Fatal("unable to migrate step 1")
err = migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable)
logging.OnError(err).Fatal("unable to migrate step 2")
err = migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance)
err = migration.Migrate(ctx, eventstoreClient, steps.FirstInstance)
logging.OnError(err).Fatal("unable to migrate step 3")
}
func initSteps(v *viper.Viper, files ...string) func() {
return func() {
for _, file := range files {
v.SetConfigFile(file)
err := v.MergeInConfig()
logging.WithFields("file", file).OnError(err).Warn("unable to read setup file")
}
for _, repeatableStep := range repeatableSteps {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
logging.OnError(err).Fatalf("unable to migrate repeatable step: %s", repeatableStep.String())
}
}

View File

@ -1,6 +1,5 @@
S3DefaultInstance:
InstanceName: Localhost
CustomDomain: localhost
FirstInstance:
InstanceName: ZITADEL
DefaultLanguage: en
Org:
Name: ZITADEL

View File

@ -20,9 +20,9 @@ import (
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/notification"
"github.com/zitadel/zitadel/internal/query/projection"
static_config "github.com/zitadel/zitadel/internal/static/config"
metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config"
tracing "github.com/zitadel/zitadel/internal/telemetry/tracing/config"
)
@ -38,6 +38,7 @@ type Config struct {
WebAuthNName string
Database database.Config
Tracing tracing.Config
Metrics metrics.Config
Projections projection.Config
Auth auth_es.Config
Admin admin_es.Config
@ -45,7 +46,6 @@ type Config struct {
OIDC oidc.Config
Login login.Config
Console console.Config
Notification notification.Config
AssetStorage static_config.AssetStorageConfig
InternalAuthZ internal_authz.Config
SystemDefaults systemdefaults.SystemDefaults
@ -65,14 +65,20 @@ func MustNewConfig(v *viper.Viper) *Config {
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
)),
)
logging.OnError(err).Fatal("unable to read config")
err = config.Log.SetLogger()
logging.OnError(err).Fatal("unable to set logger")
err = config.Tracing.NewTracer()
logging.OnError(err).Fatal("unable to set tracer")
err = config.Metrics.NewMeter()
logging.OnError(err).Fatal("unable to set meter")
return config
}

View File

@ -81,7 +81,7 @@ Requirements:
func startZitadel(config *Config, masterKey string) error {
ctx := context.Background()
dbClient, err := database.Connect(config.Database)
dbClient, err := database.Connect(config.Database, false)
if err != nil {
return fmt.Errorf("cannot start client for projection: %w", err)
}
@ -100,12 +100,12 @@ func startZitadel(config *Config, masterKey string) error {
return fmt.Errorf("cannot start eventstore for queries: %w", err)
}
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, keys.OIDC, config.InternalAuthZ.RolePermissionMappings)
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, config.InternalAuthZ.RolePermissionMappings)
if err != nil {
return fmt.Errorf("cannot start queries: %w", err)
}
authZRepo, err := authz.Start(queries, dbClient, keys.OIDC)
authZRepo, err := authz.Start(queries, dbClient, keys.OIDC, config.ExternalSecure)
if err != nil {
return fmt.Errorf("error starting authz repo: %w", err)
}
@ -139,7 +139,7 @@ func startZitadel(config *Config, masterKey string) error {
return fmt.Errorf("cannot start commands: %w", err)
}
notification.Start(config.Notification, config.ExternalPort, config.ExternalSecure, commands, queries, dbClient, assets.HandlerPrefix, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS)
notification.Start(ctx, config.Projections.Customizations["notifications"], config.ExternalPort, config.ExternalSecure, commands, queries, eventstoreClient, assets.AssetAPI(config.ExternalSecure), config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS)
router := mux.NewRouter()
tlsConfig, err := config.TLS.Config()
@ -175,10 +175,10 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
if err != nil {
return fmt.Errorf("error starting admin repo: %w", err)
}
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.Database.Database, config.DefaultInstance)); err != nil {
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.Database.Database(), config.DefaultInstance)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database, commands, queries, adminRepo, config.ExternalSecure, keys.User)); err != nil {
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database(), commands, queries, config.SystemDefaults, adminRepo, config.ExternalSecure, keys.User)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
@ -189,7 +189,8 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, instanceInterceptor.Handler))
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, instanceInterceptor.Handler, assetsCache.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
if err != nil {
@ -213,7 +214,7 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
}
apis.RegisterHandler(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
}

View File

@ -22,9 +22,17 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"],
"styles": ["src/styles.scss"],
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"],
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss"
],
"scripts": [
"./node_modules/tinycolor2/dist/tinycolor-min.js"
],
"allowedCommonJsDependencies": [
"@angular/common/locales/de",
"codemirror/mode/javascript/javascript",
@ -122,15 +130,27 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"],
"styles": ["./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", "src/styles.scss"],
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"]
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css",
"src/styles.scss"
],
"scripts": [
"./node_modules/tinycolor2/dist/tinycolor-min.js"
]
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
},
"e2e": {

View File

@ -1,14 +0,0 @@
{
"supportFile": "./cypress/support/index.ts",
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/results",
"overwrite": false,
"html": true,
"json": true
},
"chromeWebSecurity": false,
"experimentalSessionSupport": true,
"trashAssetsBeforeRuns": false
}

View File

@ -1,21 +0,0 @@
#!/usr/bin/env bash
ACTION=$1
ENVFILE=$2
shift
shift
projectRoot=".."
set -a; source $ENVFILE; set +a
NPX=""
if ! command -v cypress &> /dev/null; then
NPX="npx"
fi
$NPX cypress $ACTION \
--port ${E2E_CYPRESS_PORT} \
--env org="${E2E_ORG}",org_owner_password="${E2E_ORG_OWNER_PW}",org_owner_viewer_password="${E2E_ORG_OWNER_VIEWER_PW}",org_project_creator_password="${E2E_ORG_PROJECT_CREATOR_PW}",login_policy_user_password="${E2E_LOGIN_POLICY_USER_PW}",password_complexity_user_password="${E2E_PASSWORD_COMPLEXITY_USER_PW}",consoleUrl=${E2E_CONSOLE_URL},apiUrl="${E2E_API_URL}",accountsUrl="${E2E_ACCOUNTS_URL}",issuerUrl="${E2E_ISSUER_URL}",serviceAccountKey="${E2E_SERVICEACCOUNT_KEY}",serviceAccountKeyPath="${E2E_SERVICEACCOUNT_KEY_PATH}",otherZitadelIdpInstance="${E2E_OTHER_ZITADEL_IDP_INSTANCE}",zitadelProjectResourceId="${E2E_ZITADEL_PROJECT_RESOURCE_ID}" \
"$@"

View File

@ -1,50 +0,0 @@
import { login, User } from "../../support/login/users";
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from "../../support/api/projects";
import { apiAuth } from "../../support/api/apiauth";
describe('applications', () => {
const testProjectName = 'e2eprojectapplication'
const testAppName = 'e2eappundertest'
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
beforeEach(`ensure it doesn't exist already`, () => {
login(user)
apiAuth().then(api => {
ensureProjectExists(api, testProjectName).then(projectID => {
ensureProjectResourceDoesntExist(api, projectID, Apps, testAppName).then(() => {
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projectID}`)
})
})
})
})
it('add app', () => {
cy.get('mat-spinner')
cy.get('mat-spinner').should('not.exist')
cy.get('[data-e2e="app-card-add"]').should('be.visible').click()
// select webapp
cy.get('[formcontrolname="name"]').type(testAppName)
cy.get('[for="WEB"]').click()
cy.get('[data-e2e="continue-button-nameandtype"]').click()
//select authentication
cy.get('[for="PKCE"]').click()
cy.get('[data-e2e="continue-button-authmethod"]').click()
//enter URL
cy.get('cnsl-redirect-uris').eq(0).type("https://testurl.org")
cy.get('cnsl-redirect-uris').eq(1).type("https://testlogouturl.org")
cy.get('[data-e2e="continue-button-redirecturis"]').click()
cy.get('[data-e2e="create-button"]').click().then(() => {
cy.get('[id*=overlay]').should('exist')
})
cy.get('.data-e2e-success')
cy.wait(200)
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
//TODO: check client ID/Secret
})
})
})
})

View File

@ -1,83 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('humans', () => {
const humansPath = `${Cypress.env('consoleUrl')}/users?type=human`;
const testHumanUserNameAdd = 'e2ehumanusernameadd';
const testHumanUserNameRemove = 'e2ehumanusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(humansPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testHumanUserNameAdd);
});
});
it('should add a user', () => {
cy.get('a[href="/users/create"]').click();
cy.url().should('contain', 'users/create');
cy.get('[formcontrolname="email"]').type(username('e2ehuman'));
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testHumanUserNameAdd, { force: true });
cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname');
cy.get('[formcontrolname="lastName"]').type('e2ehumanlastname');
cy.get('[formcontrolname="phone"]').type('+41 123456789');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureHumanUserExists(api, testHumanUserNameRemove);
});
});
it('should delete a human user', () => {
cy.contains('tr', testHumanUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testHumanUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
/*
describe("users", ()=> {
before(()=> {
cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
})
it('should show personal information', () => {
cy.log(`USER: show personal information`);
//click on user information
cy.get('a[href*="users/me"').eq(0).click()
cy.url().should('contain', '/users/me')
})
it('should show users', () => {
cy.visit(Cypress.env('consoleUrl') + '/users/list/humans')
cy.url().should('contain', 'users/list/humans')
})
})
*/

View File

@ -1,60 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('machines', () => {
const machinesPath = `${Cypress.env('consoleUrl')}/users?type=machine`;
const testMachineUserNameAdd = 'e2emachineusernameadd';
const testMachineUserNameRemove = 'e2emachineusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(machinesPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testMachineUserNameAdd);
});
});
it('should add a machine', () => {
cy.get('a[href="/users/create-machine"]').click();
cy.url().should('contain', 'users/create-machine');
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testMachineUserNameAdd, { force: true });
cy.get('[formcontrolname="name"]').type('e2emachinename');
cy.get('[formcontrolname="description"]').type('e2emachinedescription');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureMachineUserExists(api, testMachineUserNameRemove);
});
});
it('should delete a machine', () => {
cy.contains('tr', testMachineUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testMachineUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});

View File

@ -1,78 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
import { login, User } from '../../support/login/users';
describe('projects', () => {
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDeleteList = 'e2eprojectdeletelist';
const testProjectNameDeleteGrid = 'e2eprojectdeletegrid';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
});
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => {
apiAuth().then((api) => {
ensureProjectDoesntExist(api, testProjectNameCreate);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('should add a project', () => {
cy.get('.add-project-button').click({ force: true });
cy.get('input').type(testProjectNameCreate);
cy.get('[data-e2e="continue-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove project', () => {
describe('list view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteList);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.get('[data-e2e=toggle-grid]').click();
cy.get('[data-cy=timestamp]');
cy.contains('tr', testProjectNameDeleteList, { timeout: 1000 })
.find('[data-e2e=delete-project-button]')
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('grid view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteGrid);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.contains('[data-e2e=grid-card]', testProjectNameDeleteGrid)
.find('[data-e2e=delete-project-button]')
.trigger('mouseover')
.click();
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
});

View File

@ -1,85 +0,0 @@
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
import { Policy, resetPolicy } from "../../support/api/policies";
import { login, User } from "../../support/login/users";
describe("private labeling", ()=> {
const orgPath = `${Cypress.env('consoleUrl')}/org`
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
let api: apiCallProperties
beforeEach(()=> {
login(user)
cy.visit(orgPath)
// TODO: Why force?
cy.contains('[data-e2e=policy-card]', 'Private Labeling').contains('button', 'Modify').click({force: true}) // TODO: select data-e2e
})
customize('white', user)
customize('dark', user)
})
})
})
function customize(theme: string, user: User) {
describe(`${theme} theme`, () => {
beforeEach(() => {
apiAuth().then(api => {
resetPolicy(api, Policy.Label)
})
})
describe.skip('logo', () => {
beforeEach('expand logo category', () => {
cy.contains('[data-e2e=policy-category]', 'Logo').click() // TODO: select data-e2e
cy.fixture('logo.png').as('logo')
})
it('should update a logo', () => {
cy.get('[data-e2e=image-part-logo]').find('input').then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png')
const file = new File([blob], 'images/logo.png', { type: 'image/png' })
const list = new DataTransfer()
list.items.add(file)
const myFileList = list.files
el[0].files = myFileList
el[0].dispatchEvent(new Event('change', { bubbles: true }))
})
})
it('should delete a logo')
})
it('should update an icon')
it('should delete an icon')
it.skip('should update the background color', () => {
cy.contains('[data-e2e=color]', 'Background Color').find('button').click() // TODO: select data-e2e
cy.get('color-editable-input').find('input').clear().type('#ae44dc')
cy.get('[data-e2e=save-colors-button]').click()
cy.get('[data-e2e=header-user-avatar]').click()
cy.contains('Logout All Users').click() // TODO: select data-e2e
login(User.LoginPolicyUser, true, null, () => {
cy.pause()
})
})
it('should update the primary color')
it('should update the warning color')
it('should update the font color')
it('should update the font style')
it('should hide the loginname suffix')
it('should show the loginname suffix')
it('should hide the watermark')
it('should show the watermark')
it('should show the current configuration')
it('should reset the policy')
})
}

View File

@ -1,15 +0,0 @@
import { readFileSync } from "fs";
module.exports = (on, config) => {
require('cypress-terminal-report/src/installLogsPrinter')(on);
config.defaultCommandTimeout = 10_000
config.env.parsedServiceAccountKey = config.env.serviceAccountKey
if (config.env.serviceAccountKeyPath) {
config.env.parsedServiceAccountKey = JSON.parse(readFileSync(config.env.serviceAccountKeyPath, 'utf-8'))
}
return config
}

View File

@ -1,49 +0,0 @@
import { sign } from 'jsonwebtoken'
export interface apiCallProperties {
authHeader: string
mgntBaseURL: string
}
export function apiAuth(): Cypress.Chainable<apiCallProperties> {
const apiUrl = Cypress.env('apiUrl')
const issuerUrl = Cypress.env('issuerUrl')
const zitadelProjectResourceID = (<string>Cypress.env('zitadelProjectResourceId')).replace('bignumber-', '')
const key = Cypress.env("parsedServiceAccountKey")
const now = new Date().getTime()
const iat = Math.floor(now / 1000)
const exp = Math.floor(new Date(now + 1000 * 60 * 55).getTime() / 1000) // 55 minutes
const bearerToken = sign({
iss: key.userId,
sub: key.userId,
aud: `${issuerUrl}`,
iat: iat,
exp: exp
}, key.key, {
header: {
alg: "RS256",
kid: key.keyId
}
})
return cy.request({
method: 'POST',
url: `${apiUrl}/oauth/v2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope: `openid urn:zitadel:iam:org:project:id:${zitadelProjectResourceID}:aud`,
assertion: bearerToken,
}
}).its('body.access_token').then(token => {
return <apiCallProperties>{
authHeader: `Bearer ${token}`,
mgntBaseURL: `${apiUrl}/management/v1/`,
}
})
}

View File

@ -1,164 +0,0 @@
export enum User {
OrgOwner = 'org_owner',
OrgOwnerViewer = 'org_owner_viewer',
OrgProjectCreator = 'org_project_creator',
LoginPolicyUser = 'login_policy_user',
PasswordComplexityUser = 'password_complexity_user',
IAMAdminUser = "zitadel-admin"
}
export function login(user:User, force?: boolean, pw?: string, onUsernameScreen?: () => void, onPasswordScreen?: () => void, onAuthenticated?: () => void): void {
let creds = credentials(user, pw)
const accountsUrl: string = Cypress.env('accountsUrl')
const consoleUrl: string = Cypress.env('consoleUrl')
const otherZitadelIdpInstance: boolean = Cypress.env('otherZitadelIdpInstance')
cy.session(creds.username, () => {
const cookies = new Map<string, string>()
if (otherZitadelIdpInstance) {
cy.intercept({
method: 'GET',
url: `${accountsUrl}/login*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('login')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/loginname*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('loginName')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/password*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('password')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/success*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('success')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize/callback*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('callback')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize*`,
times: 1,
}, (req) => {
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
})
}
cy.visit(`${consoleUrl}/loginname`, { retryOnNetworkFailure: true });
otherZitadelIdpInstance && cy.wait('@login')
onUsernameScreen ? onUsernameScreen() : null
cy.get('#loginName').type(creds.username)
cy.get('#submit-button').click()
otherZitadelIdpInstance && cy.wait('@loginName')
onPasswordScreen ? onPasswordScreen() : null
cy.get('#password').type(creds.password)
cy.get('#submit-button').click()
onAuthenticated ? onAuthenticated() : null
otherZitadelIdpInstance && cy.wait('@callback')
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
}, {
validate: () => {
if (force) {
throw new Error("clear session");
}
cy.visit(`${consoleUrl}/users/me`)
}
})
}
export function username(withoutDomain: string, project?: string): string {
return `${withoutDomain}@${project ? `${project}.` : ''}${host(Cypress.env('apiUrl')).replace('api.', '')}`
}
function credentials(user: User, pw?: string) {
const isAdmin = user == User.IAMAdminUser
return {
username: username(isAdmin ? user : `${user}_user_name`, isAdmin ? 'caos-ag' : Cypress.env('org')),
password: pw ? pw : Cypress.env(`${user}_password`)
}
}
function updateCookies(newCookies: string[] | undefined, currentCookies: Map<string, string>) {
if (newCookies === undefined) {
return
}
newCookies.forEach(cs => {
cs.split('; ').forEach(cookie => {
const idx = cookie.indexOf('=')
currentCookies.set(cookie.substring(0,idx), cookie.substring(idx+1))
})
})
}
function requestCookies(currentCookies: Map<string, string>): string[] {
let list: Array<string> = []
currentCookies.forEach((val, key) => {
list.push(key+"="+val)
})
return list
}
export function host(url: string): string {
return stripPort(stripProtocol(url))
}
function stripPort(s: string): string {
const idx = s.indexOf(":")
return idx === -1 ? s : s.substring(0,idx)
}
function stripProtocol(url: string): string {
return url.replace('http://', '').replace('https://', '')
}

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View File

@ -1,14 +0,0 @@
E2E_CYPRESS_PORT=5003
E2E_ORG=e2e-tests
E2E_ORG_OWNER_PW=Password1!
E2E_ORG_OWNER_VIEWER_PW=Password1!
E2E_ORG_PROJECT_CREATOR_PW=Password1!
E2E_PASSWORD_COMPLEXITY_USER_PW=Password1!
E2E_LOGIN_POLICY_USER_PW=Password1!
E2E_SERVICEACCOUNT_KEY_PATH="${projectRoot}/.keys/e2e.json"
E2E_CONSOLE_URL="http://localhost:4200"
E2E_API_URL="http://localhost:50002"
E2E_ACCOUNTS_URL="http://localhost:50003"
E2E_ISSUER_URL="http://localhost:50002/oauth/v2"
E2E_OTHER_ZITADEL_IDP_INSTANCE=false
E2E_ZITADEL_PROJECT_RESOURCE_ID="bignumber-$(echo -n $(docker compose -f ${projectRoot}/build/local/docker-compose-local.yml exec --no-TTY db cockroach sql --insecure --execute "select aggregate_id from eventstore.events where event_type = 'project.added' and event_data = '{\"name\": \"Zitadel\"}';" --format tsv) | cut -d " " -f 2)"

12957
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,22 @@
"start": "ng serve",
"build": "ng build",
"prodbuild": "ng build --configuration production --base-href=/ui/console/",
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss",
"e2e": "./cypress.sh run e2e.env",
"e2e:open": "./cypress.sh open e2e.env"
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.0.1",
"@angular/cdk": "^14.0.1",
"@angular/common": "^14.0.1",
"@angular/compiler": "^14.0.1",
"@angular/core": "^14.0.1",
"@angular/forms": "^14.0.1",
"@angular/material": "^14.0.1",
"@angular/material-moment-adapter": "^14.0.1",
"@angular/platform-browser": "^14.0.1",
"@angular/platform-browser-dynamic": "^14.0.1",
"@angular/router": "^14.0.1",
"@angular/service-worker": "^14.0.1",
"@angular/animations": "^14.0.4",
"@angular/cdk": "^14.0.4",
"@angular/common": "^14.0.4",
"@angular/compiler": "^14.0.4",
"@angular/core": "^14.0.4",
"@angular/forms": "^14.0.4",
"@angular/material": "^14.0.4",
"@angular/material-moment-adapter": "^14.0.4",
"@angular/platform-browser": "^14.0.4",
"@angular/platform-browser-dynamic": "^14.0.4",
"@angular/router": "^14.0.4",
"@angular/service-worker": "^14.0.4",
"@ctrl/ngx-codemirror": "^5.1.1",
"@grpc/grpc-js": "^1.5.7",
"@ngx-translate/core": "^14.0.0",
@ -36,7 +34,7 @@
"codemirror": "^5.65.0",
"cors": "^2.8.5",
"file-saver": "^2.0.5",
"google-proto-files": "^2.5.0",
"google-proto-files": "^3.0.0",
"google-protobuf": "^3.19.4",
"grpc-web": "^1.3.0",
"libphonenumber-js": "^1.10.6",
@ -52,34 +50,30 @@
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.0.1",
"@angular-eslint/builder": "^14.0.0-alpha.3",
"@angular-eslint/eslint-plugin": "^14.0.0-alpha.3",
"@angular-eslint/eslint-plugin-template": "^14.0.0-alpha.3",
"@angular-eslint/schematics": "^14.0.0-alpha.3",
"@angular-eslint/template-parser": "^14.0.0-alpha.3",
"@angular/cli": "^14.0.1",
"@angular/compiler-cli": "^14.0.1",
"@angular/language-service": "^14.0.1",
"@angular-devkit/build-angular": "^14.0.4",
"@angular-eslint/builder": "^14.0.0",
"@angular-eslint/eslint-plugin": "^14.0.0",
"@angular-eslint/eslint-plugin-template": "^14.0.0",
"@angular-eslint/schematics": "^14.0.0",
"@angular-eslint/template-parser": "^14.0.0",
"@angular/cli": "^14.0.4",
"@angular/compiler-cli": "^14.0.4",
"@angular/language-service": "^14.0.4",
"@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10",
"@types/jsonwebtoken": "^8.5.5",
"@types/node": "^17.0.42",
"@typescript-eslint/eslint-plugin": "5.25.0",
"@typescript-eslint/parser": "5.27.0",
"@typescript-eslint/eslint-plugin": "5.30.4",
"@typescript-eslint/parser": "5.30.4",
"codelyzer": "^6.0.0",
"cypress": "^10.1.0",
"cypress-terminal-report": "^4.0.1",
"eslint": "^8.17.0",
"jasmine-core": "~4.1.1",
"eslint": "^8.18.0",
"jasmine-core": "~4.2.0",
"jasmine-spec-reporter": "~7.0.0",
"jsonwebtoken": "^8.5.1",
"karma": "~6.3.16",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~5.0.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"mochawesome": "^7.1.2",
"prettier": "^2.4.1",
"protractor": "~7.0.0",
"stylelint": "^13.10.0",

View File

@ -13,6 +13,10 @@ const routes: Routes = [
loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule),
canActivate: [AuthGuard],
},
{
path: 'signedout',
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
},
{
path: 'orgs',
loadChildren: () => import('./pages/org-list/org-list.module').then((m) => m.OrgListModule),
@ -38,13 +42,8 @@ const routes: Routes = [
{
path: 'users',
canActivate: [AuthGuard],
children: [
{
path: '',
loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
},
],
},
{
path: 'instance',
loadChildren: () => import('./pages/instance/instance.module').then((m) => m.InstanceModule),
@ -170,10 +169,6 @@ const routes: Routes = [
roles: ['policy.read'],
},
},
{
path: 'signedout',
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
},
{
path: '**',
redirectTo: '/',

View File

@ -1,10 +1,9 @@
<ng-container *ngIf="(authService.user | async) || undefined as user">
<ng-container *ngIf="['iam.read$', 'iam.write$'] | hasRole as iamuser$">
<div class="main-container">
<div class="main-container">
<ng-container *ngIf="(authService.user | async) || {} as user">
<cnsl-header
*ngIf="user"
*ngIf="user && user !== {}"
[org]="org"
[user]="user"
[user]="$any(user)"
[isDarkTheme]="componentCssClass === 'dark-theme'"
[labelpolicy]="labelpolicy"
(changedActiveOrg)="changedOrg($event)"
@ -14,12 +13,14 @@
id="mainnav"
class="nav"
[ngClass]="{ shadow: yoffset > 60 }"
*ngIf="user"
*ngIf="user && user !== {}"
[org]="org"
[user]="user"
[user]="$any(user)"
[isDarkTheme]="componentCssClass === 'dark-theme'"
[labelpolicy]="labelpolicy"
></cnsl-nav>
</ng-container>
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
<div class="outlet">
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
@ -27,6 +28,4 @@
</div>
<span class="fill-space"></span>
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
</div>
</ng-container>
</ng-container>
</div>

View File

@ -69,6 +69,7 @@ export class AppComponent implements OnDestroy {
private activatedRoute: ActivatedRoute,
@Inject(DOCUMENT) private document: Document,
) {
this.themeService.loadPrivateLabelling(true);
console.log(
'%cWait!',
'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px',

View File

@ -15,6 +15,9 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { QuicklinkModule } from 'ngx-quicklink';
import { from, Observable } from 'rxjs';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { UserGuard } from 'src/app/guards/user.guard';
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
import { AssetService } from 'src/app/services/asset.service';
@ -26,7 +29,6 @@ import { HeaderModule } from './modules/header/header.module';
import { KeyboardShortcutsModule } from './modules/keyboard-shortcuts/keyboard-shortcuts.module';
import { NavModule } from './modules/nav/nav.module';
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
import { SignedoutComponent } from './pages/signedout/signedout.component';
import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
import { AdminService } from './services/admin.service';
import { AuthenticationService } from './services/authentication.service';
@ -79,23 +81,13 @@ const authConfig: AuthConfig = {
};
@NgModule({
declarations: [AppComponent, SignedoutComponent],
declarations: [AppComponent],
imports: [
AppRoutingModule,
CommonModule,
BrowserModule,
HeaderModule,
OAuthModule.forRoot({
resourceServer: {
allowedUrls: [
'https://test.api.zitadel.caos.ch/caos.zitadel.auth.api.v1.AuthService',
'https://test.api.zitadel.caos.ch/oauth/v2/userinfo',
'https://test.api.zitadel.caos.ch/caos.zitadel.management.api.v1.ManagementService/',
'https://preview.api.zitadel.caos.ch',
],
sendAccessToken: true,
},
}),
OAuthModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@ -121,6 +113,9 @@ const authConfig: AuthConfig = {
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
],
providers: [
AuthGuard,
RoleGuard,
UserGuard,
ThemeService,
{
provide: APP_INITIALIZER,

View File

@ -1,16 +1,23 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Directive({
selector: '[cnslHasRole]',
})
export class HasRoleDirective {
export class HasRoleDirective implements OnDestroy {
private destroy$: Subject<void> = new Subject();
private hasView: boolean = false;
@Input() public set hasRole(roles: string[] | RegExp[] | undefined) {
if (roles && roles.length > 0) {
this.authService.isAllowed(roles).subscribe((isAllowed) => {
this.authService
.isAllowed(roles)
.pipe(takeUntil(this.destroy$))
.subscribe((isAllowed) => {
if (isAllowed && !this.hasView) {
if (this.viewContainerRef.length !== 0) {
this.viewContainerRef.clear();
}
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
@ -19,7 +26,9 @@ export class HasRoleDirective {
});
} else {
if (!this.hasView) {
if (this.viewContainerRef.length !== 0) {
this.viewContainerRef.clear();
}
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
}
@ -30,4 +39,9 @@ export class HasRoleDirective {
protected templateRef: TemplateRef<any>,
protected viewContainerRef: ViewContainerRef,
) {}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -4,13 +4,12 @@ import { AuthConfig } from 'angular-oauth2-oidc';
import { Observable } from 'rxjs';
import { AuthenticationService } from '../services/authentication.service';
import { GrpcAuthService } from '../services/grpc-auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private auth: AuthenticationService, private authService: GrpcAuthService) {}
constructor(private auth: AuthenticationService) {}
public canActivate(
route: ActivatedRouteSnapshot,

View File

@ -1,12 +1,16 @@
<div *ngIf="type !== ActionKeysType.ORGSWITCHER && (isHandset$ | async) === false" class="action-keys-wrapper"
[ngSwitch]="type" [ngClass]="{'without-margin': withoutMargin, 'no-contrast-mode': doNotUseContrast}">
<div
*ngIf="type !== ActionKeysType.ORGSWITCHER && (isHandset$ | async) === false"
class="action-keys-wrapper"
[ngSwitch]="type"
[ngClass]="{ 'without-margin': withoutMargin, 'no-contrast-mode': doNotUseContrast }"
>
<div *ngSwitchCase="ActionKeysType.CLEAR" class="action-keys-row">
<div class="action-key esc">
<div class="key-overlay"></div>
<span>ESC</span>
</div>
</div>
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row">
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row" data-e2e="action-key-add">
<div class="action-key">
<div class="key-overlay"></div>
<span>N</span>
@ -38,7 +42,6 @@
<div class="action-key">
<div class="key-overlay"></div>
<span *ngIf="isMacLike || isIOS; else otherOS"></span>
</div>
+
<div class="action-key">

View File

@ -1,16 +1,15 @@
<div class="card" [ngClass]="{'nomargin': nomargin, 'stretch': stretch, 'warn': warn}" [attr.data-e2e]="'app-card'">
<div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}">
<div class="card" [ngClass]="{ nomargin: nomargin, stretch: stretch, warn: warn }" data-e2e="app-card">
<div *ngIf="title || description" class="header" [ngClass]="{ 'bottom-margin': expanded }">
<div *ngIf="title" class="row">
<h2 class="title" [attr.data-e2e]="'app-card-title'">{{title}}</h2>
<h2 class="title" data-e2e="app-card-title">{{ title }}</h2>
<span class="fill-space"></span>
<ng-content select="[card-actions]"></ng-content>
<button class="button" type="button" matTooltip="Expand or collapse" mat-icon-button
(click)="expanded = !expanded">
<button class="button" type="button" matTooltip="Expand or collapse" mat-icon-button (click)="expanded = !expanded">
<mat-icon *ngIf="!expanded">keyboard_arrow_down</mat-icon>
<mat-icon *ngIf="expanded">keyboard_arrow_up</mat-icon>
</button>
</div>
<p *ngIf="description" class="desc cnsl-secondary-text">{{description}}</p>
<p *ngIf="description" class="desc cnsl-secondary-text">{{ description }}</p>
</div>
<div class="card-content" *ngIf="expanded" [@openClose]="animate">
<ng-content></ng-content>

View File

@ -19,6 +19,7 @@ export enum ChangeType {
USER = 'user',
ORG = 'org',
PROJECT = 'project',
PROJECT_GRANT= 'project-grant',
APP = 'app',
}
@ -93,6 +94,9 @@ export class ChangesComponent implements OnInit, OnDestroy {
case ChangeType.PROJECT:
first = this.mgmtUserService.listProjectChanges(this.id, 30, 0);
break;
case ChangeType.PROJECT_GRANT:
first = this.mgmtUserService.listProjectGrantChanges(this.id, this.secId, 30, 0);
break;
case ChangeType.ORG:
first = this.mgmtUserService.listOrgChanges(30, 0);
break;
@ -126,6 +130,9 @@ export class ChangesComponent implements OnInit, OnDestroy {
case ChangeType.PROJECT:
more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
break;
case ChangeType.PROJECT_GRANT:
more = this.mgmtUserService.listProjectGrantChanges(this.id, this.secId, 20, cursor);
break;
case ChangeType.ORG:
more = this.mgmtUserService.listOrgChanges(20, cursor);
break;

View File

@ -9,7 +9,7 @@
.ng-untouched {
.cnsl-error {
visibility: hidden;
transition: visibility .2 ease;
transition: visibility 0.2 ease;
}
}
@ -24,10 +24,14 @@
[cnslSuffix] {
position: absolute;
right: .5rem;
right: 0.5rem;
top: 9px;
height: inherit;
vertical-align: middle;
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
input {

View File

@ -196,6 +196,9 @@
</a>
</div>
<ng-container
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
>
<div class="account-card-wrapper">
<button
cdkOverlayOrigin
@ -206,9 +209,6 @@
>
<cnsl-avatar
id="avatartoggle"
*ngIf="
user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))
"
class="avatar-toggle dontcloseonclick"
[active]="showAccount"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
@ -246,5 +246,6 @@
>
</cnsl-accounts-card>
</ng-template>
</ng-container>
</div>
</mat-toolbar>

View File

@ -48,6 +48,66 @@
</div>
</div>
<div class="info-row" *ngIf="instance">
<div class="info-wrapper">
<p class="info-row-title">{{ 'IAM.PAGES.STATE' | translate }}</p>
<p
*ngIf="instance && instance.state !== undefined"
class="state"
[ngClass]="{
active: instance.state === State.INSTANCE_STATE_RUNNING,
inactive: instance.state === State.INSTANCE_STATE_STOPPED || instance.state === State.INSTANCE_STATE_STOPPING
}"
>
{{ 'IAM.STATE.' + instance.state | translate }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="instance && instance.id" class="info-row-desc">{{ instance.id }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'NAME' | translate }}</p>
<p *ngIf="instance && instance.name" class="info-row-desc">{{ instance.name }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'VERSION' | translate }}</p>
<p *ngIf="instance && instance.version" class="info-row-desc">{{ instance.version }}</p>
</div>
<div class="info-wrapper width">
<p class="info-row-title">{{ 'IAM.PAGES.DOMAINLIST' | translate }}</p>
<div class="copy-row" *ngFor="let domain of instance?.domainsList">
<button
[disabled]="copied === domain.domain"
[matTooltip]="(copied !== domain.domain ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="domain.domain"
(copiedValue)="copied = $event"
>
{{ domain.domain }}
</button>
</div>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.creationDate" class="info-row-desc">
{{ instance.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.changeDate" class="info-row-desc">
{{ instance.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
<div class="info-row" *ngIf="org">
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p>

View File

@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core';
import { App, AppState } from 'src/app/proto/generated/zitadel/app_pb';
import { IDP, IDPState } from 'src/app/proto/generated/zitadel/idp_pb';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
@ -13,12 +14,14 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
export class InfoRowComponent {
@Input() public user!: User.AsObject;
@Input() public org!: Org.AsObject;
@Input() public instance!: InstanceDetail.AsObject;
@Input() public app!: App.AsObject;
@Input() public idp!: IDP.AsObject;
@Input() public project!: Project.AsObject;
@Input() public grantedProject!: GrantedProject.AsObject;
public UserState: any = UserState;
public State: any = State;
public OrgState: any = OrgState;
public AppState: any = AppState;
public IDPState: any = IDPState;

View File

@ -6,9 +6,7 @@ import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { InstanceMembersDataSource } from 'src/app/pages/instance/instance-members/instance-members-datasource';
import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource';
import {
ProjectGrantMembersDataSource,
} from 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-members-datasource';
import { ProjectGrantMembersDataSource } from 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-members-datasource';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { getMembershipColor } from 'src/app/utils/color';
@ -86,7 +84,7 @@ export class MembersTableComponent implements OnInit, OnDestroy {
const newRoles = Object.assign([], member.rolesList);
const index = newRoles.findIndex((r) => r === role);
if (index > -1) {
newRoles.splice(index);
newRoles.splice(index, 1);
member.rolesList = newRoles;
this.updateRoles.emit({ member: member, change: newRoles });
}

View File

@ -0,0 +1,6 @@
<button class="nav-toggle" [ngClass]="{active}" (click)="clicked.emit()">
<div class="c_label">
<span> {{ label }} </span>
<small *ngIf="count" class="count">({{ count }})</small>
</div>
</button>

View File

@ -0,0 +1,71 @@
@use '@angular/material' as mat;
@mixin nav-toggle-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: mat.get-color-from-palette($primary, 500);
$warn-color: mat.get-color-from-palette($warn, 500);
$accent-color: mat.get-color-from-palette($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
.nav-toggle {
display: flex;
align-items: center;
font-size: 14px;
line-height: 14px;
padding: 0.4rem 12px;
color: mat.get-color-from-palette($foreground, text) !important;
transition: all 0.2s ease;
text-decoration: none;
border-radius: 50vw;
border: none;
font-weight: 400;
margin: 0.25rem 2px;
white-space: nowrap;
position: relative;
background: none;
cursor: pointer;
font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
.c_label {
display: flex;
align-items: center;
text-align: center;
.count {
display: none;
margin-left: 6px;
}
}
&.external-link {
padding-right: 2rem;
i {
position: absolute;
right: 8px;
font-size: 1.2rem;
}
}
&:hover {
background: if($is-dark-theme, #ffffff40, #00000010);
}
&.active {
background-color: $primary-color;
color: mat.get-color-from-palette($foreground, toolbar-items) !important;
.c_label {
.count {
display: inline-block;
}
}
}
}
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavToggleComponent } from './nav-toggle.component';
describe('NavToggleComponent', () => {
let component: NavToggleComponent;
let fixture: ComponentFixture<NavToggleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NavToggleComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(NavToggleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'cnsl-nav-toggle',
templateUrl: './nav-toggle.component.html',
styleUrls: ['./nav-toggle.component.scss'],
})
export class NavToggleComponent {
@Input() public label: string = '';
@Input() public count: number | null = 0;
@Input() public active: boolean = false;
@Output() public clicked: EventEmitter<void> = new EventEmitter<void>();
constructor() {}
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NavToggleComponent } from './nav-toggle.component';
@NgModule({
declarations: [NavToggleComponent],
imports: [CommonModule, RouterModule],
exports: [NavToggleComponent],
})
export class NavToggleModule {}

View File

@ -3,7 +3,7 @@
<p class="length">
<span>{{ length }} </span>{{ 'PAGINATOR.COUNT' | translate }}
</p>
<p class="ts cnsl-secondary-text" *ngIf="timestamp">
<p class="ts cnsl-secondary-text" *ngIf="timestamp" data-e2e="timestamp">
{{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM YYYY, HH:mm' }}
</p>
</div>

View File

@ -8,7 +8,7 @@
<input cnslInput name="sid" formControlName="sid" />
</cnsl-form-field>
<cnsl-form-field class="sms-form-field" label="Token">
<cnsl-form-field *ngIf="token" class="sms-form-field" label="Token">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.TOKEN' | translate }}</cnsl-label>
<input cnslInput name="token" formControlName="token" />
</cnsl-form-field>

View File

@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import {
AddSMSProviderTwilioRequest,
@ -41,13 +41,14 @@ export class DialogAddSMSProviderComponent {
) {
this.twilioForm = this.fb.group({
sid: ['', [Validators.required]],
token: ['', [Validators.required]],
senderNumber: ['', [Validators.required]],
});
this.smsProviders = data.smsProviders;
if (!!this.twilio) {
this.twilioForm.patchValue(this.twilio);
} else {
this.twilioForm.addControl('token', new FormControl('', Validators.required));
}
}
@ -82,14 +83,16 @@ export class DialogAddSMSProviderComponent {
});
dialogRef.afterClosed().subscribe((token: string) => {
if (token) {
if (token && this.twilioProvider?.id) {
const tokenReq = new UpdateSMSProviderTwilioTokenRequest();
tokenReq.setToken(token);
tokenReq.setId(this.twilioProvider.id);
this.service
.updateSMSProviderTwilioToken(tokenReq)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.TOKENSET', true);
this.dialogRef.close();
})
.catch((error) => {
this.toast.showError(error);
@ -110,6 +113,15 @@ export class DialogAddSMSProviderComponent {
return this.twilioForm.get('sid');
}
public get twilioProvider(): SMSProvider.AsObject | undefined {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider) {
return twilioProvider;
} else {
return undefined;
}
}
public get twilio(): TwilioConfig.AsObject | undefined {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider && !!twilioProvider.twilio) {

View File

@ -75,9 +75,32 @@
active: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}"
></span>
>{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio?.state | translate }}</span
>
<span class="fill-space"></span>
<button
*ngIf="twilio && twilio.id"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-stroked-button
(click)="toggleSMSProviderState(twilio.id)"
>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
'ACTIONS.DEACTIVATE' | translate
}}</span>
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
'ACTIONS.ACTIVATE' | translate
}}</span>
</button>
<button
*ngIf="twilio && twilio.id"
color="warn"
[disabled]="(['iam.write'] | hasRole | async) === false"
mat-icon-button
(click)="removeSMSProvider(twilio.id)"
>
<i class="las la-trash"></i>
</button>
<button [disabled]="(['iam.write'] | hasRole | async) === false" mat-icon-button (click)="editSMSProvider()">
<i class="las la-pen"></i>
</button>

View File

@ -49,6 +49,7 @@
display: flex;
flex-direction: row;
align-items: center;
width: 350px;
.title {
font-size: 16px;

View File

@ -16,6 +16,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
@ -185,11 +186,12 @@ export class NotificationSettingsComponent implements OnInit {
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
if (req) {
if (this.hasTwilio) {
if (!!this.twilio) {
this.service
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
@ -199,6 +201,7 @@ export class NotificationSettingsComponent implements OnInit {
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
@ -234,6 +237,59 @@ export class NotificationSettingsComponent implements OnInit {
});
}
public toggleSMSProviderState(id: string): void {
const provider = this.smsProviders.find((p) => p.id === id);
if (provider) {
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
this.service
.deactivateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
this.service
.activateSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}
public removeSMSProvider(id: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.service
.removeSMSProvider(id)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio);
}
@ -257,13 +313,4 @@ export class NotificationSettingsComponent implements OnInit {
public get host(): AbstractControl | null {
return this.form.get('host');
}
public get hasTwilio(): boolean {
const twilioProvider: SMSProvider.AsObject | undefined = this.smsProviders.find((p) => p.twilio);
if (twilioProvider && !!twilioProvider.twilio) {
return true;
} else {
return false;
}
}
}

View File

@ -13,6 +13,7 @@ import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component';
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
@ -31,6 +32,7 @@ import { PasswordDialogComponent } from './password-dialog/password-dialog.compo
InputModule,
MatIconModule,
FormFieldModule,
WarnDialogModule,
MatSelectModule,
MatProgressSpinnerModule,
MatSelectModule,

View File

@ -1,4 +1,4 @@
<div [attr.data-e2e]="'color'">
<div data-e2e="color">
<p class="name cnsl-secondary-text">{{ name }}</p>
<div class="wrapper">

View File

@ -101,7 +101,7 @@
<div *ngIf="previewData && data" class="lab-policy-content">
<mat-accordion class="settings">
<mat-expansion-panel class="expansion">
<mat-expansion-panel-header [attr.data-e2e]="'policy-category'">
<mat-expansion-panel-header data-e2e="policy-category">
<mat-panel-title>
<div class="panel-title">
<i class="icon las la-image"></i>
@ -127,7 +127,7 @@
{{ 'POLICY.PRIVATELABELING.EMAILNOSVG' | translate }}
</cnsl-info-section>
<div class="logo-view" [attr.data-e2e]="'image-part-logo'">
<div class="logo-view" data-e2e="image-part-logo">
<span class="label cnsl-secondary-text">Logo</span>
<div class="img-wrapper">
<ng-container
@ -198,7 +198,7 @@
</div>
</div>
<div class="logo-view" [attr.data-e2e]="'image-part-icon'">
<div class="logo-view" data-e2e="image-part-icon">
<span class="label cnsl-secondary-text">Icon</span>
<div class="img-wrapper icon">
<ng-container
@ -478,7 +478,7 @@
</mat-expansion-panel>
<mat-expansion-panel class="expansion">
<mat-expansion-panel-header class="header" [attr.data-e2e]="'policy-category'">
<mat-expansion-panel-header class="header" data-e2e="policy-category">
<mat-panel-title>
<div class="panel-title">
<i class="icon las la-font"></i>

View File

@ -1,15 +1,25 @@
<cnsl-refresh-table [showSelectionActionButton]="showSelectionActionButton" *ngIf="projectId"
(refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult ?? 0"
[emitRefreshOnPreviousRoutes]="['/projects/'+projectId+'/roles/create']" [selection]="selection"
[loading]="dataSource?.loading$ | async" [timestamp]="dataSource?.viewTimestamp">
<cnsl-refresh-table
[showSelectionActionButton]="showSelectionActionButton"
*ngIf="projectId"
(refreshed)="refreshPage()"
[dataSize]="dataSource?.totalResult ?? 0"
[emitRefreshOnPreviousRoutes]="['/projects/' + projectId + '/roles/create']"
[selection]="selection"
[loading]="dataSource?.loading$ | async"
[timestamp]="dataSource?.viewTimestamp"
>
<ng-template cnslHasRole [hasRole]="['project.role.write:' + projectId, 'project.role.write']" actions>
<a *ngIf="actionsVisible" [disabled]="disabled" [routerLink]="[ '/projects', projectId, 'roles', 'create']"
color="primary" class="cnsl-action-button" mat-raised-button [attr.data-e2e]="'add-new-role'">
<mat-icon class="icon">add</mat-icon>
<a
*ngIf="actionsVisible"
[disabled]="disabled"
[routerLink]="['/projects', projectId, 'roles', 'create']"
color="primary"
class="cnsl-action-button"
mat-raised-button
>
<mat-icon data-e2e="add-new-role" class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', projectId, 'roles', 'create'])">
</cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="gotoRouterLink(['/projects', projectId, 'roles', 'create'])"> </cnsl-action-keys>
</a>
</ng-template>
@ -17,55 +27,70 @@
<table [dataSource]="dataSource" mat-table class="table" matSort aria-label="Elements">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null"
<mat-checkbox
[disabled]="disabled"
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" [disabled]="disabled" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<mat-checkbox
color="primary"
[disabled]="disabled"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.KEY' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.KEY' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<div class="role-key">
<cnsl-project-role-chip [roleName]="role.key">{{ role.key }}
</cnsl-project-role-chip>
<cnsl-project-role-chip [roleName]="role.key">{{ role.key }}</cnsl-project-role-chip>
</div>
</td>
</ng-container>
<ng-container matColumnDef="displayname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }} </th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.displayName}} </td>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">{{ role.displayName }}</td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.GROUP' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.GROUP' | translate }}</th>
<td mat-cell *matCellDef="let role" class="pointer">
<span class="role state" [ngClass]="{'no-selection': !selectionAllowed}" *ngIf="role.group"
<span
class="role state"
[ngClass]="{ 'no-selection': !selectionAllowed }"
*ngIf="role.group"
(click)="selectionAllowed ? selectAllOfGroup(role.group) : openDetailDialog(role)"
[matTooltip]="selectionAllowed ? ('PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role) : null">{{role.group}}</span>
[matTooltip]="selectionAllowed ? ('PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role) : null"
>{{ role.group }}</span
>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.CREATIONDATE' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span *ngIf="role?.details?.creationDate">{{role.details.creationDate | timestampToDate |
localizedDate: 'dd. MMM, HH:mm' }}</span>
<span *ngIf="role?.details?.creationDate">{{
role.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CHANGEDATE' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.ROLE.CHANGEDATE' | translate }}</th>
<td class="pointer" (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span *ngIf="role?.details?.changeDate">{{role.details.changeDate | timestampToDate |
localizedDate: 'dd. MMM, HH:mm' }}</span>
<span *ngIf="role?.details?.changeDate">{{
role.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
</td>
</ng-container>
@ -73,9 +98,16 @@
<th mat-header-cell *matHeaderCellDef class="role-table-actions"></th>
<td mat-cell *matCellDef="let role" class="role-table-actions">
<cnsl-table-actions>
<button actions
[disabled]="disabled || (['project.role.delete', 'project.role.delete:' + projectId] | hasRole | async) === false"
mat-icon-button color="warn" matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteRole(role)">
<button
actions
[disabled]="
disabled || (['project.role.delete', 'project.role.delete:' + projectId] | hasRole | async) === false
"
mat-icon-button
color="warn"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
(click)="deleteRole(role)"
>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
@ -83,17 +115,22 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let role; columns: displayedColumns;">
</tr>
<tr class="highlight" mat-row *matRowDef="let role; columns: displayedColumns"></tr>
</table>
</div>
<div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.ROLE.EMPTY' | translate}}</span>
<span>{{ 'PROJECT.ROLE.EMPTY' | translate }}</span>
</div>
<cnsl-paginator #paginator [timestamp]="dataSource?.viewTimestamp" [length]="dataSource.totalResult" [pageSize]="50"
(page)="changePage()" [pageSizeOptions]="[25, 50, 100, 250]">
<cnsl-paginator
#paginator
[timestamp]="dataSource?.viewTimestamp"
[length]="dataSource.totalResult"
[pageSize]="50"
(page)="changePage()"
[pageSizeOptions]="[25, 50, 100, 250]"
>
</cnsl-paginator>
</cnsl-refresh-table>

View File

@ -7,9 +7,10 @@
display: flex;
align-items: center;
padding-bottom: 0.5rem;
box-sizing: border-box;
&.border-bottom {
padding-bottom: 0.5rem;
padding-bottom: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid map-get($foreground, divider);
}

View File

@ -14,7 +14,7 @@
: []
"
>
<div class="p-item card" @policy [attr.data-e2e]="'policy-card'">
<div class="p-item card" @policy data-e2e="policy-card">
<div class="avatar {{ setting.color }}">
<mat-icon *ngIf="setting.svgIcon" class="mat-icon" [svgIcon]="setting.svgIcon"></mat-icon>
<i *ngIf="setting.icon" class="icon {{ setting.icon }}"></i>

View File

@ -2,8 +2,14 @@
<div class="cnsl-table-action">
<ng-content select="[hoverActions]"></ng-content>
<ng-content select="[actions]"></ng-content>
<button (click)="$event.stopPropagation()" *ngIf="hasActions" class="table-actions-trigger" mat-icon-button
[matMenuTriggerFor]="actions">
<button
(click)="$event.stopPropagation()"
*ngIf="hasActions"
class="table-actions-trigger"
mat-icon-button
[matMenuTriggerFor]="actions"
data-e2e="table-actions-button"
>
<mat-icon>more_horiz</mat-icon>
</button>

View File

@ -10,7 +10,7 @@
}}</cnsl-info-section>
<cnsl-form-field *ngIf="data.confirmation && data.confirmationKey" class="formfield">
<cnsl-label>{{ data.confirmationKey | translate: { value: data.confirmation } }}</cnsl-label>
<input cnslInput [(ngModel)]="confirm" [attr.e2e-data]="'confirm-dialog-input'" />
<input cnslInput [(ngModel)]="confirm" data-e2e="confirm-dialog-input" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
@ -24,7 +24,7 @@
mat-raised-button
class="ok-button"
(click)="closeDialogWithSuccess()"
[attr.e2e-data]="'confirm-dialog-button'"
data-e2e="confirm-dialog-button"
>
{{ data.confirmKey | translate }}
</button>

View File

@ -1,22 +1,28 @@
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="refreshPage()"
[dataSize]="dataSource?.data?.length ?? 0" [timestamp]="actionsResult?.details?.viewTimestamp"
[selection]="selection">
<cnsl-refresh-table
[hideRefresh]="true"
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource?.data?.length ?? 0"
[timestamp]="actionsResult?.details?.viewTimestamp"
[selection]="selection"
>
<div actions *ngIf="selection.isEmpty()">
<a class="cnsl-action-button" color="primary" mat-raised-button (click)="openAddAction()">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</div>
<div actions *ngIf="!selection.isEmpty()">
<button class="margin-right action-state-btn cnsl-action-button bg-state inactive" mat-raised-button
(click)="deactivateSelection()">
<button
class="margin-right action-state-btn cnsl-action-button bg-state inactive"
mat-raised-button
(click)="deactivateSelection()"
>
<span>{{ 'ACTIONS.DEACTIVATE' | translate }}</span>
<cnsl-action-keys (actionTriggered)="deactivateSelection()" [type]="ActionKeysType.DEACTIVATE">
</cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="deactivateSelection()" [type]="ActionKeysType.DEACTIVATE"> </cnsl-action-keys>
</button>
<button class="action-state-btn cnsl-action-button bg-state active" mat-raised-button (click)="activateSelection()">
<span>{{ 'ACTIONS.REACTIVATE' | translate }}</span>
<cnsl-action-keys (actionTriggered)="activateSelection()" [type]="ActionKeysType.REACTIVATE">
</cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="activateSelection()" [type]="ActionKeysType.REACTIVATE"> </cnsl-action-keys>
</button>
</div>
@ -24,48 +30,61 @@
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef class="action-select-cell">
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
<mat-checkbox
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let key" class="action-select-cell">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(key) : null" [checked]="selection.isSelected(key)">
<mat-checkbox
color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(key) : null"
[checked]="selection.isSelected(key)"
>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.ID' | translate }} </th>
<td mat-cell *matCellDef="let action" class="pointer"> {{ action?.id }} </td>
<th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.ID' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer">{{ action?.id }}</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.NAME' | translate }} </th>
<td mat-cell *matCellDef="let action" class="pointer"> {{ action?.name }} </td>
<th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.NAME' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer">{{ action?.name }}</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.STATE' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.STATE' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer">
<span class="state"
[ngClass]="{'active': action.state === ActionState.ACTION_STATE_ACTIVE,'inactive': action.state === ActionState.ACTION_STATE_INACTIVE }">
{{'FLOWS.STATES.'+action.state | translate}}</span>
<span
class="state"
[ngClass]="{
active: action.state === ActionState.ACTION_STATE_ACTIVE,
inactive: action.state === ActionState.ACTION_STATE_INACTIVE
}"
>
{{ 'FLOWS.STATES.' + action.state | translate }}</span
>
</td>
</ng-container>
<ng-container matColumnDef="timeout">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.TIMEOUT' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.TIMEOUT' | translate }}</th>
<td mat-cell *matCellDef="let key" class="pointer">
{{key.timeout | durationToSeconds}}
{{ key.timeout | durationToSeconds }}
</td>
</ng-container>
<ng-container matColumnDef="allowedToFail">
<th mat-header-cell *matHeaderCellDef> {{ 'FLOWS.ALLOWEDTOFAIL' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'FLOWS.ALLOWEDTOFAIL' | translate }}</th>
<td mat-cell *matCellDef="let action" class="pointer">
{{action.allowedToFail}}
{{ action.allowedToFail }}
</td>
</ng-container>
@ -73,8 +92,14 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let action" class="pointer">
<cnsl-table-actions>
<button [disabled]="(['action.write'] | hasRole | async) === false" actions
matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" (click)="deleteAction(action)" mat-icon-button>
<button
[disabled]="(['action.write'] | hasRole | async) === false"
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="$event.stopPropagation(); deleteAction(action)"
mat-icon-button
>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
@ -82,12 +107,17 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let action; columns: displayedColumns;" (click)="openDialog(action)">
</tr>
<tr class="highlight" mat-row *matRowDef="let action; columns: displayedColumns" (click)="openDialog(action)"></tr>
</table>
</div>
<cnsl-paginator #paginator class="paginator" [timestamp]="actionsResult?.details?.viewTimestamp"
[length]="actionsResult?.details?.totalResult || 0" [pageSize]="20" [pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"></cnsl-paginator>
<cnsl-paginator
#paginator
class="paginator"
[timestamp]="actionsResult?.details?.viewTimestamp"
[length]="actionsResult?.details?.totalResult || 0"
[pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
></cnsl-paginator>
</cnsl-refresh-table>

View File

@ -1,14 +1,14 @@
<div class="iam-top">
<div class="max-width-container">
<div class="iam-top-row">
<div>
<div class="iam-title-row">
<h1 class="iam-title">{{ 'IAM.TITLE' | translate }}</h1>
</div>
<p class="iam-sub cnsl-secondary-text">{{ 'IAM.DESCRIPTION' | translate }}</p>
</div>
<span class="fill-space"></span>
<cnsl-top-view
[hasBackButton]="false"
title="{{ 'IAM.TITLE' | translate }}"
sub="{{ 'IAM.DESCRIPTION' | translate }}"
[isActive]="instance?.state === State.STATE_RUNNING"
[isInactive]="instance?.state === State.STATE_STOPPED || instance?.state === State.STATE_STOPPING"
[hasContributors]="true"
stateTooltip="{{ 'INSTANCE.STATE.' + instance?.state | translate }}"
>
<cnsl-contributors
topContributors
[totalResult]="totalMemberResult"
[loading]="loading$ | async"
[membersSubject]="membersSubject"
@ -20,13 +20,14 @@
[disabled]="(['iam.member.write'] | hasRole | async) === false"
>
</cnsl-contributors>
</div>
</div>
</div>
<div class="max-width-container">
<h2 class="org-table-title">{{ 'ORG.LIST.TITLE' | translate }}</h2>
<p class="org-table-desc cnsl-secondary-text">{{ 'ORG.LIST.DESCRIPTION' | translate }}</p>
<cnsl-info-row topContent *ngIf="instance" [instance]="instance"></cnsl-info-row>
</cnsl-top-view>
<div class="max-width-container">
<h2 class="instance-table-title">{{ 'ORG.LIST.TITLE' | translate }}</h2>
<p class="instance-table-desc cnsl-secondary-text">{{ 'ORG.LIST.DESCRIPTION' | translate }}</p>
<cnsl-org-table></cnsl-org-table>

View File

@ -4,55 +4,15 @@
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
.iam-top {
border-bottom: 1px solid map-get($foreground, divider);
margin: 0 -2rem;
padding: 2rem 2rem 1rem 2rem;
background: map-get($background, metadata-section);
@media only screen and (max-width: 500px) {
margin: 0 -1rem;
}
.iam-top-row {
display: flex;
align-items: center;
padding-bottom: 1rem;
.iam-title-row {
display: flex;
align-items: center;
.iam-title {
margin: 0;
margin-right: 0.5rem;
}
}
.iam-sub {
margin: 1rem 0 0 0;
font-size: 14px;
}
.iam-top-desc {
font-size: 14px;
}
.fill-space {
flex: 1;
}
}
}
}
.org-table-title {
.instance-table-title {
font-size: 1.2rem;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-top: 2rem;
}
.org-table-desc {
.instance-table-desc {
font-size: 14px;
}

View File

@ -5,6 +5,7 @@ import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service';
@ -17,12 +18,13 @@ import { ToastService } from 'src/app/services/toast.service';
styleUrls: ['./instance.component.scss'],
})
export class InstanceComponent {
public instance!: InstanceDetail.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
public State: any = State;
constructor(
public adminService: AdminService,
private dialog: MatDialog,
@ -39,6 +41,17 @@ export class InstanceComponent {
});
breadcrumbService.setBreadcrumb([instanceBread]);
this.adminService
.getMyInstance()
.then((instanceResp) => {
if (instanceResp.instance) {
this.instance = instanceResp.instance;
}
})
.catch((error) => {
this.toast.showError(error);
});
}
public loadMembers(): void {

View File

@ -16,12 +16,14 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { ContributorsModule } from 'src/app/modules/contributors/contributors.module';
import { InfoRowModule } from 'src/app/modules/info-row/info-row.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { OrgTableModule } from 'src/app/modules/org-table/org-table.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SettingsGridModule } from 'src/app/modules/settings-grid/settings-grid.module';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
@ -43,7 +45,9 @@ import { InstanceComponent } from './instance.component';
MatCheckboxModule,
MetaLayoutModule,
MatIconModule,
TopViewModule,
MatTableModule,
InfoRowModule,
InputModule,
MatSortModule,
MatTooltipModule,

View File

@ -5,7 +5,25 @@
[isInactive]="org?.state === OrgState.ORG_STATE_INACTIVE"
[hasContributors]="true"
stateTooltip="{{ 'ORG.STATE.' + org?.state | translate }}"
[hasActions]="['org.write:' + org?.id, 'org.write$'] | hasRole | async"
>
<ng-template topActions cnslHasRole [hasRole]="['org.write:' + org?.id, 'org.write$']">
<button
mat-menu-item
*ngIf="org?.state === OrgState.ORG_STATE_ACTIVE"
(click)="changeState(OrgState.ORG_STATE_INACTIVE)"
>
{{ 'ORG.PAGES.DEACTIVATE' | translate }}
</button>
<button
mat-menu-item
*ngIf="org?.state === OrgState.ORG_STATE_INACTIVE"
(click)="changeState(OrgState.ORG_STATE_ACTIVE)"
>
{{ 'ORG.PAGES.REACTIVATE' | translate }}
</button>
</ng-template>
<cnsl-contributors
topContributors
[totalResult]="totalMemberResult"

View File

@ -7,6 +7,7 @@ import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-m
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
@ -62,6 +63,56 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
public changeState(newState: OrgState): void {
if (newState === OrgState.ORG_STATE_ACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.REACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'ORG.DIALOG.REACTIVATE.TITLE',
descriptionKey: 'ORG.DIALOG.REACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.reactivateOrg()
.then(() => {
this.toast.showInfo('ORG.TOAST.REACTIVATED', true);
this.org.state = OrgState.ORG_STATE_ACTIVE;
})
.catch((error) => {
this.toast.showError(error);
});
}
});
} else if (newState === OrgState.ORG_STATE_INACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DEACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'ORG.DIALOG.DEACTIVATE.TITLE',
descriptionKey: 'ORG.DIALOG.DEACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.deactivateOrg()
.then(() => {
this.toast.showInfo('ORG.TOAST.DEACTIVATED', true);
this.org.state = OrgState.ORG_STATE_INACTIVE;
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
private async getData(): Promise<void> {
this.mgmtService
.getMyOrg()

View File

@ -40,7 +40,7 @@
[disabled]="firstFormGroup.invalid"
color="primary"
matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'"
data-e2e="continue-button-nameandtype"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
@ -72,7 +72,7 @@
color="primary"
[disabled]="secondFormGroup.invalid"
matStepperNext
[attr.data-e2e]="'continue-button-authmethod'"
data-e2e="continue-button-authmethod"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
@ -130,7 +130,7 @@
<div class="app-create-actions">
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
<button mat-raised-button color="primary" matStepperNext data-e2e="continue-button-redirecturis">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
@ -234,13 +234,7 @@
<div class="app-create-actions">
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button
mat-raised-button
class="create-button"
color="primary"
(click)="createApp()"
[attr.data-e2e]="'create-button'"
>
<button mat-raised-button class="create-button" color="primary" (click)="createApp()" data-e2e="create-button">
{{ 'ACTIONS.CREATE' | translate }}
</button>
</div>
@ -248,7 +242,7 @@
</mat-horizontal-stepper>
<div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" [attr.data-e2e]="'create-app-wizzard-3'">
<form [formGroup]="form" (ngSubmit)="createApp()" data-e2e="create-app-wizzard-3">
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>

View File

@ -1,33 +1,53 @@
<h1 mat-dialog-title>
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
<span class="title">{{ 'APP.OIDC.CLIENTSECRET' | translate }}</span>
</h1>
<p class="desc cnsl-secondary-text">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}</p>
<div mat-dialog-content>
<div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{data.clientId}}</span>
<button color="primary" [disabled]="copied === data.clientId" matTooltip="copy to clipboard" cnslCopyToClipboard
[valueToCopy]="data.clientId" (copiedValue)="this.copied = $event" mat-icon-button>
<span class="overflow-auto"><span class="desc">ClientId:</span> {{ data.clientId }}</span>
<button
color="primary"
[disabled]="copied === data.clientId"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientId"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex">
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{data.clientSecret}}</span>
<button color="primary" [disabled]="copied === data.clientSecret" matTooltip="copy to clipboard" cnslCopyToClipboard
[valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button>
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{ data.clientSecret }}</span>
<button
color="primary"
[disabled]="copied === data.clientSecret"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientSecret"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button>
</div>
<ng-template #showNoSecretInfo>
<cnsl-info-section>{{'APP.OIDC.CLIENTSECRET_NOSECRET' | translate}}</cnsl-info-section>
<cnsl-info-section>{{ 'APP.OIDC.CLIENTSECRET_NOSECRET' | translate }}</cnsl-info-section>
</ng-template>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()"
[attr.data-e2e]="'close-dialog'">
{{'ACTIONS.CLOSE' | translate}}
<button
cdkFocusInitial
color="primary"
mat-raised-button
class="ok-button"
(click)="closeDialog()"
data-e2e="close-dialog"
>
{{ 'ACTIONS.CLOSE' | translate }}
</button>
</div>

View File

@ -1,4 +1,4 @@
<form class="redirect-uris-form" (ngSubmit)="add(redInput)" [attr.data-e2e]="'redirect-uris'">
<form class="redirect-uris-form" (ngSubmit)="add(redInput)" data-e2e="redirect-uris">
<cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label>

View File

@ -80,7 +80,7 @@
min-width: 320px;
.formfield {
flex: 1;
width: 500px;
}
button {

View File

@ -54,7 +54,7 @@
</ng-template>
<div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT_GRANT" [id]="project.projectId" [secId]="project.grantId"></cnsl-changes>
</div>
</cnsl-meta-layout>
</div>

View File

@ -12,7 +12,7 @@
<div
[routerLink]="['/projects', projectId, 'apps', app.id]"
[attr.data-e2e]="'app-card'"
data-e2e="app-card"
class="app-wrap"
*ngFor="let app of appsSubject | async"
matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
@ -37,7 +37,7 @@
<div
class="app-wrap"
*ngIf="!disabled"
[attr.data-e2e]="'app-card-add'"
data-e2e="app-card-add"
[routerLink]="['/projects', projectId, 'apps', 'create']"
>
<cnsl-app-card class="grid-card" matRipple [type]="OIDCAppType.ADD">

View File

@ -46,7 +46,7 @@
class="continue-button"
[disabled]="formArray.invalid"
type="submit"
[attr.data-e2e]="'save-button'"
data-e2e="save-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>

View File

@ -20,7 +20,7 @@
[disabled]="!project.name"
cdkFocusInitial
type="submit"
[attr.data-e2e]="'continue-button'"
data-e2e="continue-button"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>

View File

@ -47,7 +47,7 @@
*ngFor="let item of notPinned; index as i"
(click)="navigateToProject(type, $any(item), $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE }"
[attr.data-e2e]="'grid-card'"
data-e2e="grid-card"
>
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
@ -106,7 +106,7 @@
(click)="deleteProject($event, key)"
class="delete-button"
mat-icon-button
[attr.data-e2e]="'delete-project-button'"
data-e2e="delete-project-button"
>
<i class="las la-trash"></i>
</button>

View File

@ -1,81 +1,136 @@
<div class="projects-table-wrapper" *ngIf="projectType$ | async as type">
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refreshPage(type)" [dataSize]="totalResult"
[timestamp]="viewTimestamp" [selection]="selection" [loading]="loading$ | async">
<cnsl-filter-project actions *ngIf="!selection.hasValue()" (filterChanged)="applySearchQuery(type, $any($event))"
(filterOpen)="filterOpen = $event"></cnsl-filter-project>
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refreshPage(type)"
[dataSize]="totalResult"
[timestamp]="viewTimestamp"
[selection]="selection"
[loading]="loading$ | async"
>
<cnsl-filter-project
actions
*ngIf="!selection.hasValue()"
(filterChanged)="applySearchQuery(type, $any($event))"
(filterOpen)="filterOpen = $event"
></cnsl-filter-project>
<ng-template actions cnslHasRole [hasRole]="['project.create']">
<a *ngIf="type === ProjectType.PROJECTTYPE_OWNED" [routerLink]="[ '/projects', 'create']" color="primary"
mat-raised-button class="cnsl-action-button">
<a
*ngIf="type === ProjectType.PROJECTTYPE_OWNED"
[routerLink]="['/projects', 'create']"
color="primary"
mat-raised-button
class="cnsl-action-button"
>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', 'create'])">
</cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="gotoRouterLink(['/projects', 'create'])"> </cnsl-action-keys>
</a>
</ng-template>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
<mat-checkbox
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<mat-checkbox
color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td class="pointer"
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]"
mat-cell *matCellDef="let project">
<span *ngIf="project.name">{{project.name}}</span>
<span *ngIf="project.projectName">{{project.projectName}}</span>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.NAME' | translate }}</th>
<td
class="pointer"
[routerLink]="
type === ProjectType.PROJECTTYPE_OWNED
? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.name">{{ project.name }}</span>
<span *ngIf="project.projectName">{{ project.projectName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="projectOwnerName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }}</th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project.projectOwnerName}} </td>
{{ project.projectOwnerName }}
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td class="pointer"
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]"
mat-cell *matCellDef="let project">
<span class="state"
[ngClass]="{'active': project.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECT_STATE_INACTIVE}"
*ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.STATE' | translate }}</th>
<td
class="pointer"
[routerLink]="
type === ProjectType.PROJECTTYPE_OWNED
? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span
class="state"
[ngClass]="{
active: project.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE
}"
*ngIf="project.state"
>{{ 'PROJECT.STATE.' + project.state | translate }}</span
>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td class="pointer"
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]"
mat-cell *matCellDef="let project">
<span *ngIf="project.details.creationDate">{{project.details.creationDate | timestampToDate |
localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.CREATIONDATE' | translate }}</th>
<td
class="pointer"
[routerLink]="
type === ProjectType.PROJECTTYPE_OWNED
? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.details.creationDate">{{
project.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td class="pointer"
[routerLink]="type === ProjectType.PROJECTTYPE_OWNED ? ['/projects', project.id] : ['/granted-projects', project.projectId, 'grant', project.grantId]"
mat-cell *matCellDef="let project">
<span *ngIf="project.details.changeDate">{{project.details.changeDate | timestampToDate |
localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TABLE.CHANGEDATE' | translate }}</th>
<td
class="pointer"
[routerLink]="
type === ProjectType.PROJECTTYPE_OWNED
? ['/projects', project.id]
: ['/granted-projects', project.projectId, 'grant', project.grantId]
"
mat-cell
*matCellDef="let project"
>
<span *ngIf="project.details.changeDate">{{
project.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
</td>
</ng-container>
@ -83,8 +138,15 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let project">
<cnsl-table-actions>
<button actions *ngIf="project.id !== zitadelProjectId" color="warn" mat-icon-button
matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject(project.id, project.name)">
<button
actions
*ngIf="project.id !== zitadelProjectId"
color="warn"
mat-icon-button
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
(click)="deleteProject(project.id, project.name)"
data-e2e="delete-project-button"
>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
@ -92,15 +154,21 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'PROJECT.TABLE.EMPTY' | translate}}</span>
<span>{{ 'PROJECT.TABLE.EMPTY' | translate }}</span>
</div>
<cnsl-paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult" [pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]" (page)="changePage(type)"></cnsl-paginator>
<cnsl-paginator
class="paginator"
[timestamp]="viewTimestamp"
[length]="totalResult"
[pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage(type)"
></cnsl-paginator>
</cnsl-refresh-table>
</div>

View File

@ -9,24 +9,23 @@
<p class="sub cnsl-secondary-text max-width-description">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<div class="projects-controls">
<div class="project-type-actions">
<button
class="type-button"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_OWNED }"
(click)="setType(ProjectType.PROJECTTYPE_OWNED)"
>
{{ 'PROJECT.PAGES.TYPE.OWNED' | translate }} ({{ (mgmtService?.ownedProjectsCount | async) ?? 0 }})
</button>
<button
class="type-button"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED }"
(click)="setType(ProjectType.PROJECTTYPE_GRANTED)"
>
{{ 'PROJECT.PAGES.TYPE.GRANTED' | translate }} ({{ (mgmtService?.grantedProjectsCount | async) ?? 0 }})
</button>
<div class="project-toggle-group">
<cnsl-nav-toggle
label="{{ 'PROJECT.PAGES.TYPE.OWNED' | translate }}"
[count]="mgmtService.ownedProjectsCount | async"
(clicked)="setType(ProjectType.PROJECTTYPE_OWNED)"
[active]="(projectType$ | async) === ProjectType.PROJECTTYPE_OWNED"
></cnsl-nav-toggle>
<cnsl-nav-toggle
label="{{ 'PROJECT.PAGES.TYPE.GRANTED' | translate }}"
[count]="mgmtService.grantedProjectsCount | async"
(clicked)="setType(ProjectType.PROJECTTYPE_GRANTED)"
[active]="(projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED"
></cnsl-nav-toggle>
</div>
<span class="fill-space"></span>
<button class="grid-btn" (click)="grid = !grid" mat-icon-button [attr.data-e2e]="'toggle-grid'">
<button class="grid-btn" (click)="grid = !grid" mat-icon-button data-e2e="toggle-grid">
<i *ngIf="grid" class="show list view las la-th-list"></i>
<i *ngIf="!grid" class="las la-border-all"></i>
</button>

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