mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-09 10:13:42 +00:00
resolve conflicts
This commit is contained in:
commit
d6a05b3a61
3
.artifacts/zitadel/.gitignore
vendored
Normal file
3
.artifacts/zitadel/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.gitkeep
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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: ''
|
||||
|
||||
---
|
||||
|
13
.github/ISSUE_TEMPLATE/okr.md
vendored
13
.github/ISSUE_TEMPLATE/okr.md
vendored
@ -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
|
14
.github/ISSUE_TEMPLATE/story.md
vendored
14
.github/ISSUE_TEMPLATE/story.md
vendored
@ -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
|
5
.github/ISSUE_TEMPLATE/task.md
vendored
5
.github/ISSUE_TEMPLATE/task.md
vendored
@ -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
49
.github/workflows/e2e.yml
vendored
Normal 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
52
.github/workflows/release-channels.yml
vendored
Normal 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
54
.github/workflows/test-code.yml
vendored
Normal 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
20
.github/workflows/test-docs.yml
vendored
Normal 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"'
|
45
.github/workflows/zitadel-pr.yml
vendored
45
.github/workflows/zitadel-pr.yml
vendored
@ -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
3
.gitignore
vendored
@ -61,6 +61,7 @@ openapi/**/*.json
|
||||
build/local/*.env
|
||||
migrations/cockroach/migrate_cloud.go
|
||||
.notifications
|
||||
.artifacts
|
||||
/.artifacts/*
|
||||
!/.artifacts/zitadel
|
||||
/zitadel
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
@ -76,7 +74,7 @@ Make sure you use [semantic release messages format](https://github.com/angular/
|
||||
|
||||
#### Type
|
||||
|
||||
Must be one of the following:
|
||||
Must be one of the following:
|
||||
|
||||
- **feat**: New Feature
|
||||
- **fix**: Bug Fix
|
||||
@ -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
|
||||
@ -155,7 +153,7 @@ Scope can be left empty (omit the brackets) or refer to the top navigation secti
|
||||
ZITADEL loads translations from four files:
|
||||
|
||||
- [Console texts](./console/src/assets/i18n)
|
||||
- [Login interface](./internal/api/ui/login/static/i18n)
|
||||
- [Login interface](./internal/api/ui/login/static/i18n)
|
||||
- [Email notification](./internal/notification/static/i18n)
|
||||
- [Common texts](./internal/static/i18n) for success or error toasts
|
||||
|
||||
|
47
README.md
47
README.md
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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,30 +45,30 @@ HTTP1HostHeader: "host"
|
||||
WebAuthNName: ZITADEL
|
||||
|
||||
Database:
|
||||
Host: localhost
|
||||
Port: 26257
|
||||
Database: zitadel
|
||||
MaxOpenConns: 20
|
||||
MaxConnLifetime: 30m
|
||||
MaxConnIdleTime: 30m
|
||||
Options: ""
|
||||
User:
|
||||
Username: zitadel
|
||||
Password: ""
|
||||
SSL:
|
||||
Mode: disable
|
||||
RootCert: ""
|
||||
Cert: ""
|
||||
Key: ""
|
||||
|
||||
AdminUser:
|
||||
Username: root
|
||||
Password: ""
|
||||
SSL:
|
||||
Mode: disable
|
||||
RootCert: ""
|
||||
Cert: ""
|
||||
Key: ""
|
||||
cockroach:
|
||||
Host: localhost
|
||||
Port: 26257
|
||||
Database: zitadel
|
||||
MaxOpenConns: 20
|
||||
MaxConnLifetime: 30m
|
||||
MaxConnIdleTime: 30m
|
||||
Options: ""
|
||||
User:
|
||||
Username: zitadel
|
||||
Password: ""
|
||||
SSL:
|
||||
Mode: disable
|
||||
RootCert: ""
|
||||
Cert: ""
|
||||
Key: ""
|
||||
Admin:
|
||||
Username: root
|
||||
Password: ""
|
||||
SSL:
|
||||
Mode: disable
|
||||
RootCert: ""
|
||||
Cert: ""
|
||||
Key: ""
|
||||
|
||||
Machine:
|
||||
# Cloud hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified.
|
||||
@ -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"
|
||||
|
@ -9,15 +9,16 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Database database.Config
|
||||
AdminUser database.User
|
||||
Machine *id.Config
|
||||
Log *logging.Config
|
||||
Database database.Config
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
},
|
||||
}
|
||||
|
@ -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")
|
||||
},
|
||||
}
|
||||
|
@ -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")
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
60
cmd/setup/config_change.go
Normal file
60
cmd/setup/config_change.go
Normal 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"
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
S3DefaultInstance:
|
||||
InstanceName: Localhost
|
||||
CustomDomain: localhost
|
||||
FirstInstance:
|
||||
InstanceName: ZITADEL
|
||||
DefaultLanguage: en
|
||||
Org:
|
||||
Name: ZITADEL
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}" \
|
||||
"$@"
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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')
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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/`,
|
||||
}
|
||||
})
|
||||
}
|
@ -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://', '')
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
@ -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
12957
console/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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,12 +42,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'users',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
|
||||
},
|
||||
],
|
||||
loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
|
||||
},
|
||||
{
|
||||
path: 'instance',
|
||||
@ -170,10 +169,6 @@ const routes: Routes = [
|
||||
roles: ['policy.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'signedout',
|
||||
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/',
|
||||
|
@ -1,32 +1,31 @@
|
||||
<ng-container *ngIf="(authService.user | async) || undefined as user">
|
||||
<ng-container *ngIf="['iam.read$', 'iam.write$'] | hasRole as iamuser$">
|
||||
<div class="main-container">
|
||||
<cnsl-header
|
||||
*ngIf="user"
|
||||
[org]="org"
|
||||
[user]="user"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
[labelpolicy]="labelpolicy"
|
||||
(changedActiveOrg)="changedOrg($event)"
|
||||
></cnsl-header>
|
||||
<div class="main-container">
|
||||
<ng-container *ngIf="(authService.user | async) || {} as user">
|
||||
<cnsl-header
|
||||
*ngIf="user && user !== {}"
|
||||
[org]="org"
|
||||
[user]="$any(user)"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
[labelpolicy]="labelpolicy"
|
||||
(changedActiveOrg)="changedOrg($event)"
|
||||
></cnsl-header>
|
||||
|
||||
<cnsl-nav
|
||||
id="mainnav"
|
||||
class="nav"
|
||||
[ngClass]="{ shadow: yoffset > 60 }"
|
||||
*ngIf="user"
|
||||
[org]="org"
|
||||
[user]="user"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
[labelpolicy]="labelpolicy"
|
||||
></cnsl-nav>
|
||||
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
|
||||
<div class="outlet">
|
||||
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
|
||||
</div>
|
||||
<cnsl-nav
|
||||
id="mainnav"
|
||||
class="nav"
|
||||
[ngClass]="{ shadow: yoffset > 60 }"
|
||||
*ngIf="user && user !== {}"
|
||||
[org]="org"
|
||||
[user]="$any(user)"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
[labelpolicy]="labelpolicy"
|
||||
></cnsl-nav>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
|
||||
<div class="outlet">
|
||||
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
|
||||
</div>
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -1,25 +1,34 @@
|
||||
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) => {
|
||||
if (isAllowed && !this.hasView) {
|
||||
this.viewContainerRef.clear();
|
||||
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.viewContainerRef.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
});
|
||||
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();
|
||||
this.hasView = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!this.hasView) {
|
||||
this.viewContainerRef.clear();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
@ -56,4 +59,4 @@
|
||||
|
||||
<ng-template #otherOS>
|
||||
<span>crtl</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
@ -1,18 +1,17 @@
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -196,55 +196,56 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="account-card-wrapper">
|
||||
<button
|
||||
cdkOverlayOrigin
|
||||
#accounttrigger="cdkOverlayOrigin"
|
||||
class="icon-container"
|
||||
(click)="showAccount = !showAccount"
|
||||
[ngClass]="{ 'iam-user': (['iam.write$'] | hasRole | async) }"
|
||||
>
|
||||
<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 || ''"
|
||||
[forColor]="user?.preferredLoginName || ''"
|
||||
[name]="
|
||||
user.human?.profile?.displayName
|
||||
? user.human?.profile?.displayName ?? ''
|
||||
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
|
||||
"
|
||||
[size]="38"
|
||||
>
|
||||
</cnsl-avatar>
|
||||
</button>
|
||||
</div>
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="accounttrigger"
|
||||
[flexibleDimensions]="true"
|
||||
[lockPosition]="true"
|
||||
[cdkConnectedOverlayOffsetY]="10"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayPositions]="accountCardPositions"
|
||||
cdkConnectedOverlayBackdropClass="transparent-backdrop"
|
||||
[cdkConnectedOverlayOpen]="showAccount"
|
||||
(backdropClick)="showAccount = false"
|
||||
(detach)="showAccount = false"
|
||||
<ng-container
|
||||
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
|
||||
>
|
||||
<cnsl-accounts-card
|
||||
@accounts
|
||||
class="a_card"
|
||||
*ngIf="showAccount"
|
||||
(closedCard)="showAccount = false"
|
||||
[user]="user"
|
||||
[iamuser]="['iam.write$'] | hasRole | async"
|
||||
<div class="account-card-wrapper">
|
||||
<button
|
||||
cdkOverlayOrigin
|
||||
#accounttrigger="cdkOverlayOrigin"
|
||||
class="icon-container"
|
||||
(click)="showAccount = !showAccount"
|
||||
[ngClass]="{ 'iam-user': (['iam.write$'] | hasRole | async) }"
|
||||
>
|
||||
<cnsl-avatar
|
||||
id="avatartoggle"
|
||||
class="avatar-toggle dontcloseonclick"
|
||||
[active]="showAccount"
|
||||
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
|
||||
[forColor]="user?.preferredLoginName || ''"
|
||||
[name]="
|
||||
user.human?.profile?.displayName
|
||||
? user.human?.profile?.displayName ?? ''
|
||||
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
|
||||
"
|
||||
[size]="38"
|
||||
>
|
||||
</cnsl-avatar>
|
||||
</button>
|
||||
</div>
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="accounttrigger"
|
||||
[flexibleDimensions]="true"
|
||||
[lockPosition]="true"
|
||||
[cdkConnectedOverlayOffsetY]="10"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayPositions]="accountCardPositions"
|
||||
cdkConnectedOverlayBackdropClass="transparent-backdrop"
|
||||
[cdkConnectedOverlayOpen]="showAccount"
|
||||
(backdropClick)="showAccount = false"
|
||||
(detach)="showAccount = false"
|
||||
>
|
||||
</cnsl-accounts-card>
|
||||
</ng-template>
|
||||
<cnsl-accounts-card
|
||||
@accounts
|
||||
class="a_card"
|
||||
*ngIf="showAccount"
|
||||
(closedCard)="showAccount = false"
|
||||
[user]="user"
|
||||
[iamuser]="['iam.write$'] | hasRole | async"
|
||||
>
|
||||
</cnsl-accounts-card>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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>
|
71
console/src/app/modules/nav-toggle/nav-toggle.component.scss
Normal file
71
console/src/app/modules/nav-toggle/nav-toggle.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
14
console/src/app/modules/nav-toggle/nav-toggle.component.ts
Normal file
14
console/src/app/modules/nav-toggle/nav-toggle.component.ts
Normal 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() {}
|
||||
}
|
12
console/src/app/modules/nav-toggle/nav-toggle.module.ts
Normal file
12
console/src/app/modules/nav-toggle/nav-toggle.module.ts
Normal 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 {}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,10 +1,10 @@
|
||||
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,
|
||||
UpdateSMSProviderTwilioRequest,
|
||||
UpdateSMSProviderTwilioTokenRequest,
|
||||
AddSMSProviderTwilioRequest,
|
||||
UpdateSMSProviderTwilioRequest,
|
||||
UpdateSMSProviderTwilioTokenRequest,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -49,6 +49,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
|
@ -3,12 +3,12 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } fro
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { take } from 'rxjs';
|
||||
import {
|
||||
AddSMSProviderTwilioRequest,
|
||||
AddSMTPConfigRequest,
|
||||
UpdateSMSProviderTwilioRequest,
|
||||
UpdateSMTPConfigPasswordRequest,
|
||||
UpdateSMTPConfigPasswordResponse,
|
||||
UpdateSMTPConfigRequest,
|
||||
AddSMSProviderTwilioRequest,
|
||||
AddSMTPConfigRequest,
|
||||
UpdateSMSProviderTwilioRequest,
|
||||
UpdateSMTPConfigPasswordRequest,
|
||||
UpdateSMTPConfigPasswordResponse,
|
||||
UpdateSMTPConfigRequest,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div [attr.data-e2e]="'color'">
|
||||
<div data-e2e="color">
|
||||
<p class="name cnsl-secondary-text">{{ name }}</p>
|
||||
|
||||
<div class="wrapper">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</cnsl-refresh-table>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
@ -11,4 +17,4 @@
|
||||
<ng-content select="[menuActions]"></ng-content>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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-refresh-table>
|
||||
<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>
|
||||
|
@ -1,32 +1,33 @@
|
||||
<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-contributors
|
||||
[totalResult]="totalMemberResult"
|
||||
[loading]="loading$ | async"
|
||||
[membersSubject]="membersSubject"
|
||||
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
|
||||
(addClicked)="openAddMember()"
|
||||
(showDetailClicked)="showDetail()"
|
||||
(refreshClicked)="loadMembers()"
|
||||
[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>
|
||||
<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"
|
||||
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
|
||||
(addClicked)="openAddMember()"
|
||||
(showDetailClicked)="showDetail()"
|
||||
(refreshClicked)="loadMembers()"
|
||||
[disabled]="(['iam.member.write'] | hasRole | async) === false"
|
||||
>
|
||||
</cnsl-contributors>
|
||||
|
||||
<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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -80,7 +80,7 @@
|
||||
min-width: 320px;
|
||||
|
||||
.formfield {
|
||||
flex: 1;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -20,7 +20,7 @@
|
||||
[disabled]="!project.name"
|
||||
cdkFocusInitial
|
||||
type="submit"
|
||||
[attr.data-e2e]="'continue-button'"
|
||||
data-e2e="continue-button"
|
||||
>
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</div>
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user